├── gpm-dev ├── gpm-dev.bat ├── lib ├── constants │ ├── typedefs.dart │ ├── enums.dart │ └── constants.dart ├── core │ ├── logging │ │ ├── exit_codes.dart │ │ └── logger.dart │ ├── storage │ │ ├── storage_keys.dart │ │ ├── omegaui │ │ │ └── json_configurator.dart │ │ ├── gpm_instance_manager.dart │ │ └── gpm_storage.dart │ ├── lock.dart │ ├── service │ │ ├── update_service.dart │ │ ├── download_service.dart │ │ ├── installation_service.dart │ │ ├── system_service.dart │ │ ├── api_service.dart │ │ ├── package_disintegration_service.dart │ │ ├── package_registry_service.dart │ │ ├── upgrade_service.dart │ │ ├── package_integration_service.dart │ │ ├── build_service.dart │ │ └── package_service.dart │ └── provider │ │ ├── build_data_provider.dart │ │ └── compatible_asset_provider.dart ├── entity │ ├── source_entity.dart │ └── asset_entity.dart ├── extras │ ├── windows_utils.dart │ ├── linux_utils.dart │ ├── version_utils.dart │ └── extras.dart └── gpm_cli.dart ├── .idea ├── .gitignore ├── misc.xml ├── vcs.xml ├── modules.xml ├── libraries │ ├── Dart_SDK.xml │ └── Dart_Packages.xml └── cody_history.xml ├── gpm-binary-replacer-dev.bat ├── make-bin ├── .images └── app-icon.png ├── .gitignore ├── make-bin.bat ├── pubspec.yaml ├── pyro ├── linux │ ├── registry │ │ └── gpm.json │ └── install.sh └── windows │ ├── registry │ └── gpm.json │ └── install.ps1 ├── bin ├── gpm.dart └── gpm_binary_replacer.dart ├── gpm.iml ├── analysis_options.yaml ├── docs ├── BUILD_FROM_SOURCE.md └── EXAMPLES.md ├── gpm.yaml ├── README.md └── pubspec.lock /gpm-dev: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | dart run bin/gpm.dart "$@" -------------------------------------------------------------------------------- /gpm-dev.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | dart run bin/gpm.dart %* -------------------------------------------------------------------------------- /lib/constants/typedefs.dart: -------------------------------------------------------------------------------- 1 | typedef VoidCallback = void Function(); 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /gpm-binary-replacer-dev.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | dart run bin/gpm_binary_replacer.dart -------------------------------------------------------------------------------- /make-bin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dart compile exe -o gpm --target-os linux bin/gpm.dart -------------------------------------------------------------------------------- /.images/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generic-package-manager/gpm/HEAD/.images/app-icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /make-bin.bat: -------------------------------------------------------------------------------- 1 | dart compile exe -o gpm-binary-replacer.exe --target-os windows bin\gpm_binary_replacer.dart 2 | dart compile exe -o gpm.exe --target-os windows bin\gpm.dart -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/core/logging/exit_codes.dart: -------------------------------------------------------------------------------- 1 | class ExitCodes { 2 | ExitCodes._(); 3 | 4 | static const fine = 0; 5 | static const warning = 1; 6 | static const unavailable = 187; 7 | static const error = 2; 8 | } 9 | -------------------------------------------------------------------------------- /lib/constants/enums.dart: -------------------------------------------------------------------------------- 1 | enum OS { 2 | windows, 3 | linux, 4 | macos, 5 | debian, 6 | fedora, 7 | arch, 8 | unrecognized, 9 | } 10 | 11 | enum KnownLinuxDistro { 12 | debian, 13 | fedora, 14 | arch, 15 | } 16 | -------------------------------------------------------------------------------- /lib/core/storage/storage_keys.dart: -------------------------------------------------------------------------------- 1 | class StorageKeys { 2 | StorageKeys._(); 3 | static const mode = 'mode'; 4 | static const versions = 'versions'; 5 | static const installedAt = 'packageInstalledAt'; 6 | static const updatedAt = 'packageUpdatedAt'; 7 | } 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/core/logging/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:gpm/gpm_cli.dart'; 2 | 3 | void debugPrint( 4 | dynamic message, { 5 | String? tag, 6 | }) { 7 | // logs that help in debugging 8 | // can only be enabled in verbose mode 9 | if (verbose) { 10 | tag ??= 'GPM'; 11 | print("[$tag] $message"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/constants/constants.dart: -------------------------------------------------------------------------------- 1 | final class OSKeywords { 2 | OSKeywords._(); 3 | 4 | static final windows = ['win', 'windows']; 5 | static final macos = ['mac', 'macos']; 6 | static final linux = ['linux', 'amd64']; 7 | static final debian = ['deb', ...linux]; 8 | static final fedora = ['rpm', ...linux]; 9 | static final arch = ['arch', ...linux]; 10 | static final unrecognized = []; 11 | } 12 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: gpm 2 | description: A sample command-line application with basic argument parsing. 3 | version: 0.0.1 4 | repository: https://github.com/generic-package-manager/gpm 5 | 6 | environment: 7 | sdk: ^3.2.3 8 | 9 | 10 | dependencies: 11 | archive: ^3.4.10 12 | args: ^2.4.2 13 | chalkdart: ^2.2.1 14 | cli_spin: ^1.0.1 15 | cli_table: ^1.0.2 16 | http: ^1.2.0 17 | intl: ^0.19.0 18 | path: ^1.9.0 19 | yaml: ^3.1.2 20 | 21 | dev_dependencies: 22 | lints: ^2.1.0 23 | test: ^1.24.0 24 | -------------------------------------------------------------------------------- /pyro/linux/registry/gpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "generic-package-manager", 3 | "repo": "gpm", 4 | "index": 0, 5 | "type": "secondary", 6 | "tag": "v1.0.1", 7 | "explicit_version": false, 8 | "versions": [], 9 | "name": "gpm", 10 | "size": 7724976, 11 | "label": "", 12 | "browser_download_url": "https://github.com/generic-package-manager/gpm/releases/download/v1.0.0/gpm.AppImage", 13 | "content_type": "application/octet-stream", 14 | "updated_at": "2024-03-07 12:25:58.000Z", 15 | "download_count": 14, 16 | "mode": "release", 17 | "packageInstalledAt": "2024-03-07 19:19:10.573539" 18 | } 19 | -------------------------------------------------------------------------------- /pyro/windows/registry/gpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "generic-package-manager", 3 | "repo": "gpm", 4 | "index": 0, 5 | "type": "secondary", 6 | "tag": "v1.0.1", 7 | "explicit_version": false, 8 | "versions": [], 9 | "name": "gpm.exe", 10 | "size": 7724976, 11 | "label": "", 12 | "browser_download_url": "https://github.com/generic-package-manager/gpm/releases/download/v1.0.0/gpm.exe", 13 | "content_type": "application/octet-stream", 14 | "updated_at": "2024-03-07 12:25:58.000Z", 15 | "download_count": 14, 16 | "mode": "release", 17 | "packageInstalledAt": "2024-03-07 19:19:10.573539" 18 | } 19 | -------------------------------------------------------------------------------- /bin/gpm.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:gpm/core/logging/logger.dart'; 4 | import 'package:gpm/core/storage/gpm_instance_manager.dart'; 5 | import 'package:gpm/gpm_cli.dart'; 6 | 7 | void main(List arguments) async { 8 | GPMInstanceManager.registerAliveInstance(instanceID); 9 | 10 | currentIsolateReceivePort.listen((message) { 11 | debugPrint("Received message: $message"); 12 | if (message == "exit") { 13 | debugPrint("Exiting GPM Instance"); 14 | GPMInstanceManager.removeTerminatedInstance(instanceID); 15 | exit(instanceExitCode); 16 | } 17 | }); 18 | run(arguments); 19 | } 20 | -------------------------------------------------------------------------------- /gpm.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/core/lock.dart: -------------------------------------------------------------------------------- 1 | // #PENDING 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:chalkdart/chalkstrings.dart'; 6 | import 'package:gpm/core/storage/gpm_storage.dart'; 7 | import 'package:gpm/extras/extras.dart'; 8 | 9 | class Lock { 10 | static final File _lockFile = File(combinePath([GPMStorage.root, '.alive'])); 11 | 12 | static void find() { 13 | if (exists()) { 14 | print('Another instance of gpm is already running.'.red.bold); 15 | exit(401); 16 | } else { 17 | lock(); 18 | } 19 | } 20 | 21 | static bool exists() { 22 | return _lockFile.existsSync(); 23 | } 24 | 25 | static void lock() { 26 | if (!_lockFile.existsSync()) { 27 | _lockFile.createSync(); 28 | } 29 | } 30 | 31 | static void unlock() { 32 | if (_lockFile.existsSync()) { 33 | _lockFile.deleteSync(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /lib/entity/source_entity.dart: -------------------------------------------------------------------------------- 1 | class SourceEntity { 2 | final String owner; 3 | final String repo; 4 | final String commitHash; 5 | final String license; 6 | final bool explicitVersion; 7 | final DateTime installedAt; 8 | 9 | String get appID => '$owner/$repo'; 10 | 11 | factory SourceEntity.fromMap(Map map) { 12 | return SourceEntity( 13 | owner: map['owner'], 14 | repo: map['repo'], 15 | commitHash: map['commitHash'], 16 | license: map['license'] ?? "Unknown", 17 | explicitVersion: map['explicit_version'] ?? false, 18 | installedAt: DateTime.parse(map['packageInstalledAt']), 19 | ); 20 | } 21 | 22 | SourceEntity({ 23 | required this.owner, 24 | required this.repo, 25 | required this.commitHash, 26 | required this.license, 27 | required this.installedAt, 28 | this.explicitVersion = false, 29 | }); 30 | 31 | Map toMap() { 32 | return { 33 | 'owner': owner, 34 | 'repo': repo, 35 | 'commitHash': commitHash, 36 | 'license': license, 37 | 'packageInstalledAt': installedAt.toString(), 38 | 'explicit_version': explicitVersion, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/core/service/update_service.dart: -------------------------------------------------------------------------------- 1 | import '../../entity/asset_entity.dart'; 2 | import '../provider/compatible_asset_provider.dart'; 3 | import 'installation_service.dart'; 4 | 5 | class UpdateService { 6 | UpdateService._(); 7 | 8 | static Future initReleaseUpdate( 9 | String repo, 10 | CompatibleReleaseAssetProvider provider, 11 | List extensions, 12 | ReleaseAssetEntity current, 13 | bool explicitCall, 14 | ) async { 15 | // identifying target type 16 | List assets = []; 17 | if (current.type == 'primary') { 18 | assets.addAll(provider.primary); 19 | } else if (current.type == 'secondary') { 20 | assets.addAll(provider.secondary); 21 | } else { 22 | assets.addAll(provider.others); 23 | } 24 | 25 | // self-chosing target release 26 | int index = current.index; 27 | 28 | // checking availablity 29 | bool available = index >= 0 && index < assets.length; 30 | if (available) { 31 | // going for update 32 | ReleaseAssetEntity target = assets[index]; 33 | // the install service will do the rest 34 | await InstallationService.downloadRelease(target, explicitCall); 35 | } 36 | 37 | return available; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/core/service/download_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:gpm/core/service/api_service.dart'; 4 | import 'package:http/http.dart' as http; 5 | 6 | class DownloadService { 7 | DownloadService._(); 8 | 9 | static Future download({ 10 | required String url, 11 | required String path, 12 | required void Function(int progress) onProgress, 13 | required Future Function(String path) onComplete, 14 | required void Function() onError, 15 | }) async { 16 | final request = http.Request('GET', Uri.parse(url)); 17 | request.headers.addAll(getGitHubAPIHeaders() ?? {}); 18 | final response = await http.Client().send(request); 19 | if (response.statusCode == 200) { 20 | final total = response.contentLength ?? 0; 21 | int received = 0; 22 | final List bytes = []; 23 | final subscription = response.stream.listen((value) { 24 | bytes.addAll(value); 25 | received += value.length; 26 | if (total != 0) { 27 | onProgress(((received * 100) / total).round()); 28 | } 29 | }); 30 | await subscription.asFuture(); 31 | final file = File(path); 32 | file.writeAsBytesSync(bytes); 33 | await onComplete(file.absolute.path); 34 | } else { 35 | onError(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/BUILD_FROM_SOURCE.md: -------------------------------------------------------------------------------- 1 | 2 | ## Building a repository from source using gpm 3 | 4 | gpm is both a package manager and a build tool which means you can use gpm to install a repository by building it from source. 5 | 6 | For gpm, to be able to build a repository from source, it needs a file called `gpm.yaml` at the repository root. 7 | 8 | example: 9 | ```shell 10 | repository 11 | |- data 12 | |- lib 13 | |- README.md 14 | |- gpm.yaml # here 15 | ``` 16 | 17 | This file contains information about **type** of the repository that is whether the repository is a `cli` program or an `application`. 18 | 19 | Next, it contains **build** instructions on what platforms are supported by the repository and instructions on how to build the repository on each of them. 20 | 21 | Here is gpm's own [build specification](https://github.com/generic-package-manager/gpm/blob/main/gpm.yaml) to help you understand, it contains detailed description on how to write a build specification file. 22 | 23 | You can test your build specification locally without even pushing the file by using the `--build-locally` option.s 24 | 25 | ### Can I write gpm.yaml for any repo? 26 | Yes, if your repository contains a binary or script that can be installed as a package, then, you can write a build specification for it. 27 | 28 | You can even clone any repo and write a build specification for it and then you can build the repository using `gpm --build-locally userid/repo`. 29 | 30 | ### What about Desktop Shortcuts? 31 | gpm will itself create the desktop shortcut for any application that you are trying to install. -------------------------------------------------------------------------------- /lib/extras/windows_utils.dart: -------------------------------------------------------------------------------- 1 | /// [windows_utils.dart] contains Windows platform specific code 2 | /// that helps in creating a desktop shortcut i.e the windows lnk file 3 | /// using powershell scripting. 4 | 5 | import 'dart:io'; 6 | import '../core/storage/gpm_storage.dart'; 7 | import 'extras.dart'; 8 | 9 | /// [WindowsUtils] basic system calls for windows using ps1 scripts 10 | class WindowsUtils { 11 | WindowsUtils._(); 12 | 13 | /// opens up a [path] using the windows's explorer program 14 | static void openInExlorer(String path) { 15 | Process.runSync('explorer', [path], runInShell: true); 16 | } 17 | 18 | /// Creates a Windows Desktop Shortcut by writing the lnk file 19 | /// at $HOME\Desktop\[executable].lnk where [executable] is repository name. 20 | static int createDesktopShortcut(String name, String executable) { 21 | final result = Process.runSync( 22 | 'pwsh', 23 | [ 24 | '-Command', 25 | GPMStorage.windowsDesktopShortcutCreatorFile.path, 26 | ], 27 | workingDirectory: GPMStorage.root, 28 | environment: { 29 | 'DestinationPath': "${getUserHome()}\\Desktop\\$name.lnk", 30 | 'SourceExe': executable, 31 | }, 32 | ); 33 | if (result.exitCode != 0) { 34 | print(result.stdout); 35 | print(result.stderr); 36 | } 37 | return result.exitCode; 38 | } 39 | 40 | // Deletes the Windows Desktop Shortcut 41 | static void deleteDesktopShortcut(String name) { 42 | final path = "${getUserHome()}\\Desktop\\$name.lnk"; 43 | final file = File(path); 44 | if (file.existsSync()) { 45 | file.deleteSync(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | 2 | ## Cli Usage Examples 3 | 4 | Hi there! follow this guide to understand how to use gpm. 5 | 6 | As we know every repository on GitHub has a unique **owner** username. 7 | for example: omegaui/cliptopia, here **omegaui** is the username and **cliptopia** is the repo name. 8 | 9 | This technique allows multiple users to have the same repository name throughout GitHub. 10 | for example: x/repo and y/repo can co-exist as they have different owners. 11 | 12 | To install a repository using gpm, you need to put the full-qualified name of the repository you are trying to install. 13 | 14 | For example, install rufus on Windows 15 | ```shell 16 | gpm --install pbatard/rufus 17 | ``` 18 | 19 | But it's not compulsory, if you only provide the name of the repository, gpm will then query a search and bring you the 20 | top 10 results based on the number of stars on the repository. 21 | 22 | so, you can even run the same as: 23 | ```shell 24 | gpm --install rufus 25 | ``` 26 | 27 | Run `gpm --help` to see more available options and flags. 28 | 29 | ## Building a repository 30 | To build a repository from source, it should have `gpm.yaml` file at its root, a detailed set of instructions on this can 31 | be found at the [build from source guide](https://github.com/generic-package-manager/gpm/blob/main/docs/BUILD_FROM_SOURCE.md). 32 | 33 | Try running the following: 34 | ```shell 35 | gpm --build omegaui/cliptopia 36 | ``` 37 | 38 | ## Example Projects (using gpm) 39 | 40 | - [Cliptopia](https://github.com/omegaui/cliptopia): Cliptopia is a beautiful state-of-the-art clipboard management software for linux desktops that turns your simple clipboard into a full-fledged ⚡power house⚡ -------------------------------------------------------------------------------- /bin/gpm_binary_replacer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:gpm/core/storage/gpm_instance_manager.dart'; 5 | import 'package:gpm/core/storage/gpm_storage.dart'; 6 | import 'package:gpm/extras/extras.dart'; 7 | 8 | void main() async { 9 | // next we get a unique id 10 | final id = generateGPMIsolateID(); 11 | 12 | // next, we register the instance 13 | GPMInstanceManager.registerAliveReplacer(id); 14 | 15 | // creating our own isolate 16 | final isolate = await Isolate.spawn((_) => run(), id); 17 | 18 | // next, we create a receive port for the current isolate 19 | ReceivePort port = ReceivePort(id); 20 | port.listen((message) { 21 | if (message == id) { 22 | // and when the current instance finishes, 23 | // we remove it from the list 24 | GPMInstanceManager.removeTerminatedReplacer(id); 25 | } 26 | port.close(); 27 | }); 28 | isolate.addOnExitListener(port.sendPort, response: id); 29 | } 30 | 31 | void run() async { 32 | // before even starting the replacer isolate 33 | // we'll check if there is a need for it 34 | // which is basically checking that .gpm.exe exists or not 35 | 36 | final extension = Platform.isWindows ? ".exe" : ""; 37 | 38 | final targetFile = File(combinePath( 39 | [GPMStorage.appsDir.path, 'omegaui', 'gpm', '.gpm$extension'])); 40 | if (!targetFile.existsSync()) { 41 | print("Target doesn't exists: $targetFile"); 42 | exit(0); 43 | } 44 | 45 | final gpmActivityFile = File(combinePath([GPMStorage.root, 'activity.json'])); 46 | 47 | // not every file system may allow watching a file 48 | // so, we use a periodic check approach 49 | 50 | while (gpmActivityFile.existsSync()) { 51 | await Future.delayed(Duration(seconds: 1)); 52 | } 53 | 54 | // now, that every instance of gpm has terminated 55 | // we'll silently replace its binary with the updated version 56 | 57 | targetFile.renameSync(combinePath( 58 | [GPMStorage.appsDir.path, 'omegaui', 'gpm', 'gpm$extension'])); 59 | 60 | print('Replacing Finished Successfully.'); 61 | } 62 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /pyro/linux/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # GPM Installer 4 | # v1.0.0 5 | 6 | # Greetings 7 | echo "Hello There!" 8 | echo "Welcome to GPM Installer." 9 | sleep 1 10 | 11 | # Creating gpm directory structure 12 | echo "Creating gpm directory structure ..." 13 | echo "Creating ~/.gpm ..." 14 | mkdir ~/.gpm 15 | echo "Creating ~/.gpm/apps ..." 16 | mkdir ~/.gpm/apps/ 17 | echo "Creating gpm binary home directory ..." 18 | mkdir ~/.gpm/apps/generic-package-manager 19 | mkdir ~/.gpm/apps/generic-package-manager/gpm 20 | 21 | # Creating registry structure 22 | echo "Creating registry structure ..." 23 | echo "Creating ~/.gpm/registry ..." 24 | mkdir ~/.gpm/registry/ 25 | echo "Creating gpm registry home directory ..." 26 | mkdir ~/.gpm/registry/generic-package-manager 27 | 28 | 29 | # Starting to download gpm 30 | echo "Finding the latest gpm release on GitHub ..." 31 | echo "Downloading gpm ..." 32 | wget https://github.com/generic-package-manager/gpm/releases/latest/download/gpm.AppImage --output-document=$HOME/.gpm/apps/generic-package-manager/gpm/gpm 33 | chmod +x ~/.gpm/apps/generic-package-manager/gpm/gpm 34 | ~/.gpm/apps/generic-package-manager/gpm/gpm --version 35 | 36 | # Pulling Registry Information 37 | echo "Pulling Registry Information ..." 38 | echo "This will make gpm updatable using gpm itself ;)" 39 | wget -q https://raw.githubusercontent.com/generic-package-manager/gpm/master/pyro/linux/registry/gpm.json --output-document=$HOME/.gpm/registry/generic-package-manager/gpm.json 40 | 41 | # Putting gpm on PATH 42 | echo "Putting gpm on PATH ..." 43 | gpm 44 | gpm_path="$HOME/.gpm/apps/generic-package-manager/gpm" 45 | 46 | # Check if gpm is already on PATH 47 | if command -v gpm &> /dev/null; then 48 | echo "gpm is already on PATH." 49 | else 50 | # Add gpm to PATH 51 | export PATH="\$PATH:$gpm_path" 52 | echo "PATH=\"\$PATH:$gpm_path\"" >> "$HOME/.$(basename "$SHELL")rc" 53 | echo "Added gpm to PATH." 54 | fi 55 | 56 | # Verify if gpm is now on PATH 57 | if command -v gpm &> /dev/null; then 58 | echo "gpm is on PATH." 59 | 60 | # Checking for updates 61 | gpm --update generic-package-manager/gpm 62 | 63 | # Conclusion 64 | echo "Done! gpm is now installed on your system." 65 | else 66 | echo "Failed to add gpm to PATH. Please check the installation." 67 | echo "Done! gpm is now present on your system, but you may have to add it to PATH manually." 68 | fi 69 | -------------------------------------------------------------------------------- /pyro/windows/install.ps1: -------------------------------------------------------------------------------- 1 | 2 | # @echo off 3 | 4 | # GPM Installer 5 | # v1.0.0 6 | 7 | # Greetings 8 | echo "Hello There!" 9 | echo "Welcome to GPM Installer." 10 | pwsh -nop -c "& {sleep 1}" 11 | 12 | echo $HOME 13 | 14 | # Creating gpm directory structure 15 | echo "Creating gpm directory structure ..." 16 | echo "Creating ~/.gpm ..." 17 | mkdir $HOME\.gpm 18 | echo "Creating ~/.gpm/apps ..." 19 | mkdir $HOME\.gpm\apps\ 20 | echo "Creating gpm binary home directory ..." 21 | mkdir $HOME\.gpm\apps\generic-package-manager 22 | mkdir $HOME\.gpm\apps\generic-package-manager\gpm 23 | 24 | # Creating registry structure 25 | echo "Creating registry structure ..." 26 | echo "Creating ~/.gpm/registry ..." 27 | mkdir $HOME\.gpm\registry\ 28 | echo "Creating gpm registry home directory ..." 29 | mkdir $HOME\.gpm\registry\generic-package-manager 30 | 31 | # Starting to download gpm 32 | echo "Finding the latest gpm release on GitHub ..." 33 | echo "Downloading gpm ..." 34 | pwsh -NoProfile -ExecutionPolicy Bypass -Command "& { Invoke-WebRequest -Uri 'https://github.com/generic-package-manager/gpm/releases/latest/download/gpm.exe' -OutFile '$HOME\.gpm\apps\generic-package-manager\gpm\gpm.exe' }" 35 | echo "Downloading gpm update helper ..." 36 | pwsh -NoProfile -ExecutionPolicy Bypass -Command "& { Invoke-WebRequest -Uri 'https://github.com/generic-package-manager/gpm/releases/latest/download/gpm-binary-replacer.exe' -OutFile '$HOME\.gpm\apps\generic-package-manager\gpm\gpm-binary-replacer.exe' }" 37 | Start-Process -FilePath $HOME/.gpm/apps/generic-package-manager/gpm/gpm.exe -ArgumentList "--version" -Wait -NoNewWindow 38 | 39 | # Pulling Registry Information 40 | echo "Pulling Registry Information ..." 41 | echo "This will make gpm updatable using gpm itself." 42 | pwsh -NoProfile -ExecutionPolicy Bypass -Command "& { Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/generic-package-manager/gpm/master/pyro/windows/registry/gpm.json' -OutFile '$HOME\.gpm\registry\generic-package-manager\gpm.json' }" 43 | 44 | # Putting gpm on PATH 45 | 46 | echo "To ensure security on your windows operating system, " 47 | echo "gpm will not itself update the path variable." 48 | echo "Please add $HOME\.gpm\apps\generic-package-manager\gpm to your System Environment Variable to access the installed cli program throughtout your system." 49 | 50 | # Conclusion 51 | echo "Done! gpm is now present on your system, you just have to update your PATH variable." -------------------------------------------------------------------------------- /.idea/cody_history.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | </llm> 47 | </llm> 48 | </chat> 49 | <chat> 50 | <accountId value="VXNlcjoyMzk2Mzk=" /> 51 | <internalId value="9fca1601-97be-4a8f-9dd2-72409d856b2c" /> 52 | <llm> 53 | <llm> 54 | <model value="anthropic/claude-3-sonnet-20240229" /> 55 | <provider value="Anthropic" /> 56 | <title value="Claude 3 Sonnet" /> 57 | </llm> 58 | </llm> 59 | </chat> 60 | <chat> 61 | <accountId value="VXNlcjoyMzk2Mzk=" /> 62 | <internalId value="bf50bc2d-6d22-44e4-8339-c66c2e5c320c" /> 63 | <llm> 64 | <llm> 65 | <model value="anthropic/claude-3-sonnet-20240229" /> 66 | <provider value="Anthropic" /> 67 | <title value="Claude 3 Sonnet" /> 68 | </llm> 69 | </llm> 70 | </chat> 71 | </list> 72 | </chats> 73 | <defaultLlm> 74 | <llm> 75 | <model value="anthropic/claude-3-sonnet-20240229" /> 76 | <provider value="Anthropic" /> 77 | <title value="Claude 3 Sonnet" /> 78 | </llm> 79 | </defaultLlm> 80 | </component> 81 | </project> -------------------------------------------------------------------------------- /lib/entity/asset_entity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:gpm/core/service/system_service.dart'; 4 | 5 | class ReleaseAssetEntity { 6 | final String owner; 7 | final String repo; 8 | final String tag; 9 | final String name; 10 | final String label; 11 | final String license; 12 | final int size; 13 | final String downloadURL; 14 | final num downloads; 15 | final String contentType; 16 | final DateTime updatedAt; 17 | late int index; 18 | late String type; 19 | late bool explicitVersion; 20 | late List<String> versions; 21 | 22 | String get appID => '$owner/$repo'; 23 | 24 | ReleaseAssetEntity({ 25 | required this.owner, 26 | required this.repo, 27 | required this.tag, 28 | required this.name, 29 | required this.label, 30 | required this.license, 31 | required this.size, 32 | required this.downloadURL, 33 | required this.downloads, 34 | required this.contentType, 35 | required this.updatedAt, 36 | }); 37 | 38 | ReleaseAssetEntity.fromMap( 39 | this.owner, this.repo, this.tag, Map<String, dynamic> map) 40 | : name = map['name'], 41 | size = map['size'], 42 | label = map['label'] ?? "", 43 | license = map['license'] ?? "Unknown", 44 | index = map['index'] ?? 0, 45 | explicitVersion = map['explicit_version'] ?? false, 46 | versions = List<String>.from(map['versions'] ?? []), 47 | type = map['type'] ?? 'others', 48 | downloadURL = map['browser_download_url'], 49 | contentType = map['content_type'] ?? "", 50 | updatedAt = DateTime.parse(map['updated_at'] ?? DateTime.now()), 51 | downloads = map['download_count']; 52 | 53 | Map<String, dynamic> toMap() { 54 | return { 55 | 'owner': owner, 56 | 'repo': repo, 57 | 'index': index, 58 | 'type': type, 59 | 'tag': tag, 60 | 'explicit_version': explicitVersion, 61 | 'versions': versions, 62 | 'name': name, 63 | 'size': size, 64 | 'label': label, 65 | 'license': license, 66 | 'browser_download_url': downloadURL, 67 | 'content_type': contentType, 68 | 'updated_at': updatedAt.toString(), 69 | 'download_count': downloads, 70 | }; 71 | } 72 | 73 | bool isCompatibleWithOS() { 74 | final keywords = SystemService.getOSKeywords(); 75 | for (final key in keywords) { 76 | if (name.contains(key)) { 77 | return true; 78 | } 79 | } 80 | return false; 81 | } 82 | 83 | bool isSetup() { 84 | if (Platform.isWindows) { 85 | String lowerCaseName = name.toLowerCase(); 86 | 87 | bool containsSetupKeyword = lowerCaseName.contains('setup'); 88 | bool containsInstallerKeyword = lowerCaseName.contains('installer'); 89 | 90 | bool hasMSIExtension = lowerCaseName.endsWith('.msi'); 91 | bool hasEXEExtension = lowerCaseName.endsWith('.exe'); 92 | 93 | bool isSetupFile = hasMSIExtension || 94 | (hasEXEExtension && 95 | (containsSetupKeyword || containsInstallerKeyword)); 96 | return isSetupFile; 97 | } 98 | if (Platform.isLinux) { 99 | return type == 'primary'; 100 | } 101 | return false; 102 | } 103 | 104 | @override 105 | String toString() { 106 | return name; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/core/storage/omegaui/json_configurator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:chalkdart/chalkstrings.dart'; 5 | 6 | import '../../../extras/extras.dart'; 7 | import '../gpm_storage.dart'; 8 | 9 | const _jsonEncoder = JsonEncoder.withIndent(' '); 10 | 11 | class JsonConfigurator { 12 | late String configName; 13 | late String configPath; 14 | dynamic config; 15 | 16 | JsonConfigurator({ 17 | String? configName, 18 | String? configPath, 19 | this.config, 20 | bool atRoot = false, 21 | }) { 22 | this.configPath = configPath ?? 23 | combinePath([ 24 | GPMStorage.root, 25 | if (!atRoot) GPMStorage.registryStorage, 26 | configName! 27 | ]); 28 | if (configName != null) { 29 | this.configName = configName; 30 | } 31 | if (configName == null) { 32 | this.configName = this 33 | .configPath 34 | .substring(this.configPath.lastIndexOf(Platform.pathSeparator) + 1); 35 | } 36 | _load(); 37 | } 38 | 39 | void _load() { 40 | config = jsonDecode("{}"); 41 | try { 42 | File file = File(configPath); 43 | if (file.existsSync()) { 44 | config = jsonDecode(file.readAsStringSync()); 45 | } else { 46 | // Creating raw session config 47 | if (!file.parent.existsSync()) { 48 | file.parent.createSync(recursive: true); 49 | } 50 | file.createSync(); 51 | file.writeAsStringSync("{}", flush: true); 52 | onNewCreation(); 53 | } 54 | } catch (error) { 55 | print( 56 | "Permission Denied when Creating Configuration: $configName, cannot continue!" 57 | .red); 58 | rethrow; 59 | } 60 | } 61 | 62 | void onNewCreation() { 63 | // called when the config file is auto created! 64 | } 65 | 66 | void reload() { 67 | _load(); 68 | } 69 | 70 | void overwriteAndReload(String content) { 71 | File file = File(configPath); 72 | if (!file.existsSync()) { 73 | file.createSync(); 74 | } 75 | file.writeAsStringSync(content, flush: true); 76 | _load(); 77 | } 78 | 79 | void put(key, value) { 80 | config[key] = value; 81 | saveSync(); 82 | } 83 | 84 | void add(key, value) { 85 | var list = config[key]; 86 | if (list != null) { 87 | config[key] = {...list, value}.toList(); 88 | } else { 89 | config[key] = [value]; 90 | } 91 | saveSync(); 92 | } 93 | 94 | void addAll(Map<String, dynamic> map) { 95 | final entries = map.entries; 96 | for (final element in entries) { 97 | put(element.key, element.value); 98 | } 99 | } 100 | 101 | void remove(key, value) { 102 | var list = config[key]; 103 | if (list != null) { 104 | list.remove(value); 105 | config[key] = list; 106 | } else { 107 | config[key] = []; 108 | } 109 | saveSync(); 110 | } 111 | 112 | dynamic get(key) { 113 | return config[key]; 114 | } 115 | 116 | void saveSync() { 117 | try { 118 | File(configPath) 119 | .writeAsStringSync(_jsonEncoder.convert(config), flush: true); 120 | } catch (error) { 121 | print("Permission Denied when Saving Configuration: $configName"); 122 | } 123 | } 124 | 125 | void deleteSync() { 126 | config = jsonDecode("{}"); 127 | File(configPath).deleteSync(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/core/storage/gpm_instance_manager.dart: -------------------------------------------------------------------------------- 1 | /// [GPMInstanceManager] 2 | /// This code is only needed to be executed on windows operating system 3 | /// unlike linux or unix, windows doesn't allow rewriting or deleting 4 | /// the process's source file which ultimately prevents gpm 5 | /// from updating itself on windows. 6 | 7 | import 'dart:io'; 8 | 9 | import 'package:gpm/core/logging/logger.dart'; 10 | import 'package:gpm/core/storage/gpm_storage.dart'; 11 | import 'package:gpm/core/storage/omegaui/json_configurator.dart'; 12 | import 'package:gpm/extras/extras.dart'; 13 | 14 | class GPMInstanceManager { 15 | static final instancesConfig = 16 | JsonConfigurator(configName: "activity.json", atRoot: true); 17 | 18 | static final replacersConfig = 19 | JsonConfigurator(configName: "replacer-activity.json", atRoot: true); 20 | 21 | static void registerAliveInstance(id) { 22 | if (Platform.isWindows) { 23 | debugPrint('Registering instance $id'); 24 | instancesConfig.add('instances', id); 25 | } 26 | } 27 | 28 | static void removeTerminatedInstance(id) { 29 | if (Platform.isWindows) { 30 | debugPrint('Removed Terminated instance $id'); 31 | instancesConfig.remove('instances', id); 32 | if (instancesConfig.get('instances').isEmpty) { 33 | instancesConfig.deleteSync(); 34 | } 35 | } 36 | } 37 | 38 | static void registerAliveReplacer(id) { 39 | if (Platform.isWindows) { 40 | replacersConfig.add('replacers', id); 41 | } 42 | } 43 | 44 | static void removeTerminatedReplacer(id) { 45 | if (Platform.isWindows) { 46 | replacersConfig.remove('replacers', id); 47 | if (replacersConfig.get('replacers').isEmpty) { 48 | replacersConfig.deleteSync(); 49 | } 50 | } 51 | } 52 | 53 | static bool isAnyReplacerAlive() { 54 | if (Platform.isWindows) { 55 | if (replacersConfig.config != null) { 56 | return false; 57 | } 58 | final replacers = replacersConfig.get('replacers'); 59 | return replacers != null && replacers.isNotEmpty; 60 | } 61 | return false; 62 | } 63 | 64 | static Future<void> spawnBinaryReplacer() async { 65 | if (Platform.isWindows) { 66 | if (!GPMInstanceManager.isAnyReplacerAlive()) { 67 | print("Initializing GPM Update Helper ..."); 68 | await Process.start( 69 | combinePath([ 70 | GPMStorage.appsDir.path, 71 | 'omegaui', 72 | 'gpm', 73 | 'gpm-binary-replacer.exe' 74 | ]), 75 | [], 76 | mode: ProcessStartMode.detached, 77 | ); 78 | await Future.delayed(Duration(seconds: 2)); 79 | } 80 | } 81 | } 82 | 83 | static void helpTheHelper() { 84 | if (Platform.isWindows) { 85 | try { 86 | // doing just the same 87 | // the helper does to update gpm 88 | final targetFile = File(combinePath([ 89 | GPMStorage.appsDir.path, 90 | 'omegaui', 91 | 'gpm', 92 | '.gpm-binary-replacer.exe' 93 | ])); 94 | if (targetFile.existsSync()) { 95 | targetFile.renameSync(combinePath([ 96 | GPMStorage.appsDir.path, 97 | 'omegaui', 98 | 'gpm', 99 | 'gpm-binary-replacer.exe' 100 | ])); 101 | } 102 | } catch (e) { 103 | // we may encounter issue saying the helper is already running 104 | // so, we just ignore it this time 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gpm.yaml: -------------------------------------------------------------------------------- 1 | 2 | # GPM's build from source specification 3 | # v0.0.1 4 | 5 | 6 | # specify the type of repository 7 | # either your repo could be a cli program or a gui application. 8 | # [type] can be [cli] or [application] 9 | # if you set [type] to [cli] then, gpm will try to find the [executable] on [platform]'s path. 10 | # during the installation process, if not found gpm will add it to the path. 11 | # if you set [type] to [application] then, gpm will omit the environment check. 12 | 13 | type: cli 14 | 15 | # icon: <raw link to your repository/program's icon file> 16 | 17 | # the [build] parameter contains the list of platforms supported by your repository 18 | # the supported platform names are [windows], [linux] & [macos]. 19 | # If you want to target a specific linux distribution you can specify one of 20 | # [debian], [fedora], [arch] instead of [linux] 21 | # if you want to target other linux distributions like 22 | # OpenSUSE then simple specify [fedora], 23 | # or [linux] if not very common distro. 24 | 25 | build: 26 | # applicable for every platform: 27 | # OPT: the [note] parameter is used to display a message to the user before the build is started 28 | # OPT: the [executable] parameter tells gpm about the entry point of your program 29 | # (used for making desktop shortcuts and environment checks) (defaults to repository) 30 | # OPT: the [appData] to prevent deletion of files in your repository after build is completed specify them in this list. 31 | # OPT: the [dependencies] contains the list of dependencies that are required to build your repository from source along with their versions. 32 | # MAN: the [steps] contains the list of commands that are executed one by one for building your repository from source. 33 | 34 | # OPT - Optional 35 | # MAN - Mandatory 36 | 37 | - windows: 38 | note: 'Thanks for using GPM (windows).' 39 | # executable tells gpm your program's entry point 40 | executable: gpm.exe 41 | # after the build process is finished 42 | # the cloned repository is deleted 43 | # to prevent deleting the compiled files or directories 44 | # list them here, gpm will store them as app data required for your application to run at ~/.gpm/apps/yourid/reponame 45 | appData: 46 | - gpm.exe 47 | - gpm-binary-replacer.exe 48 | dependencies: 49 | - executable: pwsh 50 | # version parameter is optional 51 | version: ^7.4.0 52 | # help parameter is optional 53 | help: You can download Powershell from https://github.com/PowerShell/PowerShell/releases 54 | - executable: dart 55 | # version parameter is optional 56 | version: ^3.2.6 57 | # installCommand parameter is optional 58 | installCommand: choco install dart 59 | # help parameter is optional 60 | help: You can download Dart from https://dart.dev/get-dart 61 | steps: 62 | - name: Running pub get 63 | run: dart pub get 64 | - name: Compiling GPM Updater (Only needed for Windows OS) 65 | run: dart compile exe -o gpm-binary-replacer.exe --target-os windows bin\gpm_binary_replacer.dart 66 | - name: Compiling GPM with dart 67 | run: dart compile exe -o gpm.exe --target-os windows bin\gpm.dart 68 | - linux: 69 | note: 'Thanks for using GPM (linux).' 70 | executable: gpm 71 | appData: 72 | - gpm 73 | dependencies: 74 | - executable: dart 75 | version: ^3.2.6 76 | help: You can download Dart from https://dart.dev/get-dart 77 | steps: 78 | - name: Running pub get 79 | run: dart pub get 80 | - name: Compiling GPM with dart 81 | run: dart compile exe -o gpm --target-os linux bin/gpm.dart 82 | 83 | # the following section allows you to rename your downloaded assets 84 | # it supports renaming secondary packages on any supported platform. 85 | releases: 86 | linux: 87 | secondary: 88 | renameTo: gpm # suppose, gpm.AppImage got renamed to command convention "gpm". -------------------------------------------------------------------------------- /lib/extras/linux_utils.dart: -------------------------------------------------------------------------------- 1 | /// [linux_utils.dart] contains linux platform specific code 2 | /// that helps in identifying the linux distribution, 3 | /// creating desktop shortcuts and querying the desktop. 4 | 5 | import 'dart:io'; 6 | 7 | import '../core/service/api_service.dart'; 8 | import '../core/service/download_service.dart'; 9 | import '../core/storage/gpm_storage.dart'; 10 | import 'extras.dart'; 11 | 12 | /// [LinuxUtils] contains functions that work commonly in any linux distribution 13 | class LinuxUtils { 14 | /// Creates a desktop shortcut by writing a desktop entry 15 | /// file at ~/.local/share/applications/[executable].desktop 16 | /// where [executable] is usually the repository name 17 | /// and [id] is the package id used to fetching the icon of the app if any. 18 | static Future<int> createDesktopShortcut(String id, String executable) async { 19 | // checking if the repo has a gpm specification 20 | var icon = "https://img.icons8.com/color/128/empty-box.png"; 21 | final spec = await APIService.getGPMSpecification(id); 22 | if (spec != null) { 23 | icon = spec['icon'] ?? icon; 24 | } 25 | 26 | final iconPath = combinePath([GPMStorage.toAppDirPath(id), 'app-icon.png']); 27 | 28 | print('Fetching App Icon ...'); 29 | 30 | await DownloadService.download( 31 | url: icon, 32 | path: iconPath, 33 | onProgress: (progress) {}, 34 | onComplete: (path) async { 35 | final repo = parseRepoName(id); 36 | final shortCutData = """ 37 | [Desktop Entry] 38 | Name=$repo 39 | Exec=$executable 40 | Icon=$iconPath 41 | Type=Application 42 | Terminal=false 43 | Categories=Utility; 44 | """; 45 | print('Writing Desktop Entry ...'); 46 | final desktopEntryFile = File(combinePath([ 47 | getUserHome(), 48 | '.local', 49 | 'share', 50 | 'applications', 51 | '$repo.desktop' 52 | ])); 53 | desktopEntryFile.writeAsStringSync(shortCutData, flush: true); 54 | }, 55 | onError: () {}, 56 | ); 57 | return 0; 58 | } 59 | 60 | /// Deletes a desktop shortcut by deleting the desktop entry file 61 | static void deleteDesktopShortcut(String repo) { 62 | final desktopEntryFile = File(combinePath( 63 | [getUserHome(), '.local', 'share', 'applications', '$repo.desktop'])); 64 | if (desktopEntryFile.existsSync()) { 65 | desktopEntryFile.deleteSync(); 66 | } 67 | } 68 | 69 | /// Opens the given path in the system file manager 70 | static void openInFiles(String path) { 71 | Process.runSync('xdg-open', [path], runInShell: true); 72 | } 73 | } 74 | 75 | /// [parseLinuxSystemInfo] parses the contents of /etc/os-release file 76 | /// using [LinuxSystemReleaseDataEntity]. 77 | LinuxSystemReleaseDataEntity parseLinuxSystemInfo(contents) { 78 | return LinuxSystemReleaseDataEntity.fromReleaseFile(contents); 79 | } 80 | 81 | /// [LinuxSystemReleaseDataEntity] reads the given os-release contents 82 | /// and identifies the underlying linux distribution. 83 | /// The primary goal is to find the root parent distro 84 | /// among [debian], [fedora] and [arch]. 85 | class LinuxSystemReleaseDataEntity { 86 | final Map<String, String> _releaseData = {}; 87 | 88 | /// Reads the os-release contents 89 | /// and parses the data into _releaseData 90 | LinuxSystemReleaseDataEntity.fromReleaseFile(String contents) { 91 | List<String> lines = contents.split('\n'); 92 | for (final line in lines) { 93 | if (line.isEmpty) { 94 | continue; 95 | } 96 | final indexOfSeparator = line.indexOf('='); 97 | final key = line.substring(0, indexOfSeparator); 98 | var value = line.substring(indexOfSeparator + 1); 99 | if (value[0] == '"') { 100 | value = value.substring(1, value.length - 1); 101 | } 102 | _releaseData[key.toLowerCase()] = value; 103 | } 104 | } 105 | 106 | /// Simple function to fetch the value at the given [key] 107 | String? _get(String key) { 108 | return _releaseData[key.toLowerCase()]; 109 | } 110 | 111 | String? get id => _get('ID_LIKE') ?? _get('ID'); 112 | 113 | bool isDebian() { 114 | return id == 'debian'; 115 | } 116 | 117 | bool isFedora() { 118 | return id == 'fedora'; 119 | } 120 | 121 | bool isArch() { 122 | return id == 'arch'; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/core/service/installation_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chalkdart/chalkstrings.dart'; 4 | import 'package:cli_spin/cli_spin.dart'; 5 | import 'package:gpm/core/logging/exit_codes.dart'; 6 | import 'package:gpm/gpm_cli.dart'; 7 | 8 | import '../../constants/typedefs.dart'; 9 | import '../../entity/asset_entity.dart'; 10 | import '../../extras/extras.dart'; 11 | import '../provider/compatible_asset_provider.dart'; 12 | import '../storage/gpm_storage.dart'; 13 | import 'download_service.dart'; 14 | import 'package_integration_service.dart'; 15 | 16 | class InstallationService { 17 | InstallationService._(); 18 | 19 | static Future<void> initReleaseInstall( 20 | String repo, 21 | CompatibleReleaseAssetProvider provider, 22 | List<String> extensions, 23 | bool explicitCall, 24 | ) async { 25 | // the target release to install 26 | ReleaseAssetEntity? target; 27 | 28 | // primary Assets are considered first 29 | if (provider.hasPrimary) { 30 | target = _selectTarget(provider.primary, 'primary'); 31 | } else if (provider.hasSecondary) { 32 | target = _selectTarget(provider.secondary, 'secondary'); 33 | } else { 34 | target = _selectTarget(provider.others, 'others'); 35 | } 36 | if (target != null) { 37 | await _installRelease(target, explicitCall); 38 | } else { 39 | print("Aborting Install."); 40 | } 41 | } 42 | 43 | static ReleaseAssetEntity? _selectTarget( 44 | List<ReleaseAssetEntity> assets, String type) { 45 | int input = 1; 46 | if (assets.length > 1) { 47 | for (final asset in assets) { 48 | final index = assets.indexOf(asset) + 1; 49 | print("${"#$index".bold} - $asset (${formatBytes(asset.size)})"); 50 | } 51 | stdout.write( 52 | "Please select the target you want to install (default=${option ?? 1}): " 53 | .bold); 54 | var line = option ?? stdin.readLineSync(); 55 | if (line != null && line.isEmpty) { 56 | line = null; 57 | } 58 | input = int.tryParse(line ?? "1") ?? -1; 59 | if (input < 1 || input > assets.length) { 60 | print("Invalid Selection.".red); 61 | return null; 62 | } 63 | } else { 64 | final asset = assets.first; 65 | print("${"#1".bold} - $asset (${formatBytes(asset.size)})"); 66 | if (!yes("Proceed to install (y/N): ")) { 67 | return null; 68 | } 69 | } 70 | return assets[input - 1] 71 | ..index = input - 1 72 | ..type = type; 73 | } 74 | 75 | static Future<void> _installRelease( 76 | ReleaseAssetEntity target, bool explicitCall) async { 77 | await downloadRelease(target, explicitCall); 78 | } 79 | 80 | static Future<void> downloadRelease(ReleaseAssetEntity target, explicitCall, 81 | {VoidCallback? onComplete}) async { 82 | String generateText(int progress) { 83 | return "${"$progress %".blue.bold} Downloading $target from GitHub ... "; 84 | } 85 | 86 | final startTime = DateTime.now(); 87 | final spinner = CliSpin( 88 | text: generateText(0), 89 | spinner: CliSpinners.pipe, 90 | ).start(); 91 | 92 | // Release file path 93 | final path = GPMStorage.toPath(target); 94 | final total = target.size; 95 | 96 | // Checking if its already downloaded 97 | final file = File(path); 98 | if (file.existsSync()) { 99 | if (file.statSync().size == total) { 100 | spinner.stopAndPersist( 101 | text: '➜ ${'[OK]'.green.bold} Using cached download', 102 | ); 103 | await PackageIntegrationService.integrate(target, path, explicitCall); 104 | } 105 | } else { 106 | await DownloadService.download( 107 | url: target.downloadURL, 108 | path: path, 109 | onProgress: (progress) { 110 | spinner.text = generateText(progress); 111 | }, 112 | onComplete: (path) async { 113 | final endTime = DateTime.now(); 114 | spinner.stopAndPersist( 115 | text: 116 | '➜ ${'[OK]'.green.bold} Download Completed, ${'[took ${formatTime(endTime.difference(startTime))}]'}.', 117 | ); 118 | await PackageIntegrationService.integrate(target, path, explicitCall); 119 | }, 120 | onError: () { 121 | spinner.fail('Downloaded Failed.'.red); 122 | 123 | if(explicitCall) terminateInstance(exitCode: ExitCodes.error); 124 | }, 125 | ); 126 | } 127 | 128 | onComplete?.call(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/extras/version_utils.dart: -------------------------------------------------------------------------------- 1 | /// [version_utils.dart] has version resolving utility functions, 2 | /// at the time of building from source, it performs checks to identify 3 | /// whether the installed version of any dependency is satisfied or not. 4 | 5 | import 'dart:io'; 6 | 7 | enum VersionRequirementMode { 8 | /// uses ^ symbol 9 | /// e.g: ^1.2.3 10 | /// the installed version should be greater than or equal to 1.2.3. 11 | moreOrEqual, 12 | 13 | /// uses ~ symbol 14 | /// e.g: ~1.2.3 15 | /// the installed version's major version should be 1.x.x. 16 | about, 17 | 18 | /// uses > symbol 19 | /// e.g: >1.2.3 20 | /// the installed version should be greater than 1.2.3. 21 | greater, 22 | 23 | /// uses < symbol 24 | /// e.g: <1.2.3 25 | /// the installed version should be less than 1.2.3. 26 | lower, 27 | 28 | /// uses no symbol 29 | /// e.g: 1.2.3 30 | /// the installed version should be 1.2.3. 31 | exact, 32 | } 33 | 34 | /// Parses the version data out of the outputs of various commands 35 | /// example: [runs] dart --version 36 | /// [output] Dart SDK version: 3.2.6 (stable) (Wed Jan 24 13:41:58 2024 +0000) on "windows_x64" 37 | /// [result] 3.2.6 38 | String? getVersionString(String executable) { 39 | RegExp versionRegex = RegExp(r'\b(\d+(\.\d+)+)\b'); 40 | 41 | final result = Process.runSync( 42 | executable, 43 | ['--version'], 44 | runInShell: true, 45 | environment: Platform.environment, 46 | ); 47 | final output = result.stdout; 48 | Match? match = versionRegex.firstMatch(output); 49 | if (match != null) { 50 | return match.group(0)!; 51 | } else { 52 | return null; 53 | } 54 | } 55 | 56 | /// Checks if the required version by the dependency is installed or not. 57 | bool isVersionResolved({ 58 | required String requiredVersion, 59 | required String installedVersion, 60 | }) { 61 | // identifying requirement mode 62 | final mode = getRequirementMode(requiredVersion); 63 | if (mode != VersionRequirementMode.exact) { 64 | // removing leading symbol 65 | requiredVersion = requiredVersion.substring(1); 66 | } else { 67 | // there is no brainstorming for exact matching 68 | return requiredVersion == installedVersion; 69 | } 70 | // segmenting versions into major, minor & patch 71 | final requiredSegmentMap = _segmentVersion(requiredVersion); 72 | final installedSegmentMap = _segmentVersion(installedVersion); 73 | // parsing in advance for similar cases 74 | int installedVersionNumber = int.parse(installedSegmentMap.values.join()); 75 | int requiredVersionNumber = int.parse(requiredSegmentMap.values.join()); 76 | // checking if version could be resolved 77 | bool resolved = true; 78 | switch (mode) { 79 | case VersionRequirementMode.moreOrEqual: 80 | bool isEqual = requiredVersion == installedVersion; 81 | if (!isEqual) { 82 | if (installedVersionNumber < requiredVersionNumber) { 83 | resolved = false; 84 | } 85 | } 86 | break; 87 | case VersionRequirementMode.about: 88 | // the major versions should be same 89 | resolved = requiredSegmentMap['major'] == installedSegmentMap['major']; 90 | break; 91 | case VersionRequirementMode.greater: 92 | if (installedVersionNumber < requiredVersionNumber) { 93 | resolved = false; 94 | } 95 | break; 96 | case VersionRequirementMode.lower: 97 | if (installedVersionNumber > requiredVersionNumber) { 98 | resolved = false; 99 | } 100 | break; 101 | case VersionRequirementMode.exact: 102 | } 103 | return resolved; 104 | } 105 | 106 | /// Parses the [VersionRequirementMode] out of the dependency's [version] key 107 | VersionRequirementMode getRequirementMode(String version) { 108 | VersionRequirementMode mode = VersionRequirementMode.exact; 109 | List<String> symbols = ['^', '~', '>', '<']; 110 | final symbolChar = version[0]; 111 | final index = symbols.indexOf(symbolChar); 112 | if (index >= 0) { 113 | mode = VersionRequirementMode.values[index]; 114 | } 115 | return mode; 116 | } 117 | 118 | /// Breaks down the version into major, minor & patch 119 | /// e.g: for 1.2.3 120 | /// major: 1 121 | /// minor: 2 122 | /// patch: 3 123 | Map<String, String> _segmentVersion(String version) { 124 | final segmented = version.split('.'); 125 | String? major = segmented.elementAtOrNull(0) ?? "0"; 126 | String? minor = segmented.elementAtOrNull(1) ?? "0"; 127 | String? patch = segmented.elementAtOrNull(2) ?? "0"; 128 | return { 129 | 'major': major, 130 | 'minor': minor, 131 | 'patch': patch, 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /lib/core/provider/build_data_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:gpm/extras/version_utils.dart'; 4 | import 'package:yaml/yaml.dart'; 5 | 6 | import '../service/system_service.dart'; 7 | 8 | class BuildDataProvider { 9 | final String type; 10 | final List<PlatformBuildData> data; 11 | 12 | BuildDataProvider({required this.type, required this.data}); 13 | 14 | factory BuildDataProvider.fromMap(dynamic spec) { 15 | return BuildDataProvider( 16 | type: spec['type'], 17 | data: List<PlatformBuildData>.from( 18 | () { 19 | final supportedPlatforms = spec['build'] as YamlList; 20 | List<PlatformBuildData> platformData = []; 21 | for (final element in supportedPlatforms) { 22 | final platformMap = element.value as YamlMap; 23 | final platform = platformMap.keys.first; 24 | final map = Map<dynamic, dynamic>.fromEntries( 25 | platformMap.value[platform].entries); 26 | map['platform'] = platform; 27 | platformData.add(PlatformBuildData.fromMap(map)); 28 | } 29 | return platformData; 30 | }(), 31 | ), 32 | ); 33 | } 34 | 35 | bool isHostSupported() { 36 | if (Platform.isLinux) { 37 | final hostMime = <String>{'linux', SystemService.os}; 38 | final supported = data.any((e) => hostMime.contains(e.platform)); 39 | return supported; 40 | } 41 | return data.any((element) => element.platform == SystemService.os); 42 | } 43 | 44 | PlatformBuildData getTargetPlatformBuildInstructions() { 45 | if (Platform.isLinux) { 46 | final hostMime = <String>{'linux', SystemService.os}; 47 | final supported = data.firstWhere((e) => hostMime.contains(e.platform)); 48 | return supported; 49 | } 50 | final target = 51 | data.firstWhere((element) => element.platform == SystemService.os); 52 | return target; 53 | } 54 | } 55 | 56 | class PlatformBuildData { 57 | final String executable; 58 | final String note; 59 | final String platform; 60 | final List<String> appData; 61 | final List<DependencyData> dependencies; 62 | final List<Step> steps; 63 | 64 | bool get hasDependencies => dependencies.isNotEmpty; 65 | 66 | factory PlatformBuildData.fromMap(dynamic spec) { 67 | final List<String> appData = []; 68 | final List<DependencyData> dependencies = []; 69 | final List<Step> steps = []; 70 | 71 | // adding all to be add data files 72 | appData.addAll(List<String>.from(spec['appData'] ?? [])); 73 | 74 | // parsing dependency data 75 | final dependenciesMap = spec['dependencies'] ?? {}; 76 | for (final dependency in dependenciesMap) { 77 | dependencies.add(DependencyData.fromMap(dependency)); 78 | } 79 | 80 | // parsing steps 81 | final stepsMap = spec['steps']; 82 | for (final step in stepsMap) { 83 | steps.add(Step.fromMap(step)); 84 | } 85 | 86 | return PlatformBuildData( 87 | executable: spec['executable'] ?? '', 88 | note: spec['note'] ?? '', 89 | platform: spec['platform'], 90 | appData: appData, 91 | dependencies: dependencies, 92 | steps: steps, 93 | ); 94 | } 95 | 96 | PlatformBuildData({ 97 | required this.note, 98 | required this.executable, 99 | required this.platform, 100 | required this.appData, 101 | required this.dependencies, 102 | required this.steps, 103 | }); 104 | } 105 | 106 | class DependencyData { 107 | final String executable; 108 | final String version; 109 | final String installCommand; 110 | final String help; 111 | 112 | DependencyData({ 113 | required this.executable, 114 | required this.version, 115 | required this.installCommand, 116 | required this.help, 117 | }); 118 | 119 | bool get hasVersion => version.isNotEmpty; 120 | bool get hasInstallCommand => installCommand.isNotEmpty; 121 | bool get hasHelp => help.isNotEmpty; 122 | 123 | factory DependencyData.fromMap(map) { 124 | return DependencyData( 125 | executable: map['executable'], 126 | version: map['version'] ?? '', 127 | installCommand: map['installCommand'] ?? '', 128 | help: map['help'] ?? '', 129 | ); 130 | } 131 | 132 | bool isVersionSatisfied() { 133 | if (!hasVersion) { 134 | return true; 135 | } 136 | final version = getVersionString(executable); 137 | if (version == null) { 138 | return false; 139 | } 140 | return isVersionResolved( 141 | requiredVersion: this.version, 142 | installedVersion: version, 143 | ); 144 | } 145 | } 146 | 147 | class Step { 148 | final String name; 149 | final String run; 150 | final bool ignoreError; 151 | 152 | Step(this.name, this.run, this.ignoreError); 153 | 154 | factory Step.fromMap(dynamic map) { 155 | return Step( 156 | map['name'], 157 | map['run'], 158 | map['ignoreError'] ?? false, 159 | ); 160 | } 161 | 162 | Future<int> executeAsync(String workingDir) async { 163 | final exitCode = await SystemService.execute( 164 | run, 165 | workingDir, 166 | false, 167 | ); 168 | return exitCode; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/extras/extras.dart: -------------------------------------------------------------------------------- 1 | /// [extras.dart] contains utility functions used all around gpm 2 | /// Includes functions for: 3 | /// - Formatting 4 | /// - Parsing 5 | /// - Path Utils 6 | 7 | import 'dart:io'; 8 | import 'dart:math'; 9 | 10 | import 'package:archive/archive_io.dart'; 11 | import 'package:chalkdart/chalkstrings.dart'; 12 | 13 | /// When this field is set to [true] 14 | /// then the user will not be prompted for confirmation 15 | bool yesToAll = false; 16 | 17 | /// parses the name of the package owner 18 | /// for [omegaui/gpm] the output will be [omegaui] 19 | String parseOwnerName(String appID) { 20 | return appID.substring(0, appID.indexOf('/')); 21 | } 22 | 23 | /// parses the name of the repository 24 | /// for [omegaui/gpm] the output will be [gpm] 25 | String parseRepoName(String appID) { 26 | return appID.substring(appID.indexOf('/') + 1); 27 | } 28 | 29 | /// returns only the extension of the file entity if any 30 | /// else the entire name is returned 31 | String getExtension(String filename) { 32 | if (filename.contains('.')) { 33 | return filename.substring(filename.lastIndexOf('.') + 1); 34 | } 35 | return filename; 36 | } 37 | 38 | /// Converts [bytes] into Human Readable Form 39 | /// e.g: 1024 to "1 KB" 40 | String formatBytes(int bytes) { 41 | if (bytes <= 0) { 42 | return "0 B"; 43 | } 44 | const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 45 | var i = (log(bytes) / log(1024)).floor(); 46 | return '${(bytes / pow(1024, i)).toStringAsFixed(0)} ${suffixes[i]}'; 47 | } 48 | 49 | /// Converts [duration] into a format more like a time difference 50 | String formatTime(Duration duration) { 51 | if (duration.inMilliseconds < 1000) { 52 | return "${duration.inMilliseconds} ms"; 53 | } else if (duration.inSeconds < 60) { 54 | return "${duration.inSeconds} seconds"; 55 | } else if (duration.inMinutes < 60) { 56 | return "${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')} minutes"; 57 | } else { 58 | return "${duration.inHours}:${(duration.inMinutes % 60).toString().padLeft(2, '0')} hours"; 59 | } 60 | } 61 | 62 | /// Returns the user's home directory path 63 | String getUserHome() { 64 | String? home; 65 | if (Platform.isWindows) { 66 | home = Platform.environment['USERPROFILE']; 67 | } else if (Platform.isMacOS) { 68 | home = Platform.environment['HOME']; 69 | } else if (Platform.isLinux) { 70 | home = Platform.environment['HOME']; 71 | } 72 | home ??= Directory.systemTemp.path; 73 | return home; 74 | } 75 | 76 | /// Forms a path by combining 77 | /// the [locations] i.e the file entity names 78 | String combinePath(List<String> locations, {bool absolute = false}) { 79 | String path = locations.join(Platform.pathSeparator); 80 | return absolute ? File(path).absolute.path : path; 81 | } 82 | 83 | void movePath(String oldPath, String newPath) { 84 | try { 85 | dynamic oldEntity; 86 | 87 | if (FileSystemEntity.isDirectorySync(oldPath)) { 88 | oldEntity = Directory(oldPath); 89 | } else { 90 | oldEntity = File(oldPath); 91 | } 92 | 93 | if (oldEntity is File) { 94 | // Ensure the destination directory exists 95 | var newDirectory = Directory(newPath).parent; 96 | if (!newDirectory.existsSync()) { 97 | newDirectory.createSync(recursive: true); 98 | } 99 | var newLocation = File(newPath); 100 | if (newLocation.existsSync()) { 101 | newLocation.deleteSync(); 102 | } 103 | oldEntity.copySync(newPath); 104 | } else if (oldEntity is Directory) { 105 | // Ensure the destination directory exists 106 | var newDirectory = Directory(newPath); 107 | if (!newDirectory.existsSync()) { 108 | newDirectory.createSync(recursive: true); 109 | } else { 110 | newDirectory.deleteSync(recursive: true); 111 | newDirectory.createSync(recursive: true); 112 | } 113 | oldEntity.renameSync(newPath); 114 | } else { 115 | print("Unsupported entity type at the old path."); 116 | } 117 | } catch (e) { 118 | print("Error moving the file: $e"); 119 | } 120 | } 121 | 122 | /// Used to prompt the user 123 | /// if user enters y or Y, then true is returned else false 124 | bool yes(String text) { 125 | if (yesToAll) { 126 | return true; 127 | } 128 | stdout.write(text.bold); 129 | var input = stdin.readLineSync() ?? "n"; 130 | return input == 'y' || input == 'Y'; 131 | } 132 | 133 | /// Extracts an archive object to a destination path 134 | /// Useful at the time of source mode install 135 | Future<bool> extract(String archivePath, String outputPath) async { 136 | try { 137 | final inputStream = InputFileStream(archivePath); 138 | final archive = ZipDecoder().decodeBuffer(inputStream); 139 | await extractArchiveToDiskAsync(archive, outputPath); 140 | inputStream.close(); 141 | return true; 142 | } catch (e) { 143 | // ignore 144 | print(e); 145 | } 146 | return false; 147 | } 148 | 149 | /// Performs an addtional check when deleting a directory 150 | void deleteDir(String path) { 151 | final dir = Directory(path); 152 | if (dir.existsSync()) { 153 | dir.deleteSync(recursive: true); 154 | } 155 | } 156 | 157 | String generateGPMIsolateID() { 158 | String randomLetter = String.fromCharCode(Random().nextInt(26) + 65); 159 | String randomDigits = (100 + Random().nextInt(9999)).toString(); 160 | String randomString = '$randomLetter$randomDigits'; 161 | return randomString; 162 | } 163 | -------------------------------------------------------------------------------- /lib/core/storage/gpm_storage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../../entity/asset_entity.dart'; 4 | import '../../extras/extras.dart'; 5 | import '../service/system_service.dart'; 6 | import 'package:gpm/gpm_cli.dart'; 7 | 8 | class GPMStorage { 9 | GPMStorage._(); 10 | 11 | static const rootName = '.gpm'; 12 | static const downloadStorage = 'downloads'; 13 | static const registryStorage = 'registry'; 14 | static const appsStorage = 'apps'; 15 | static Directory gpmDir = Directory(combinePath([getUserHome(), rootName])); 16 | static Directory downloadsDir = 17 | Directory(combinePath([root, downloadStorage])); 18 | static Directory registryDir = 19 | Directory(combinePath([root, registryStorage])); 20 | static Directory appsDir = Directory(combinePath([root, appsStorage])); 21 | static File unixCommandExecutorFile = 22 | File(combinePath([root, 'unix-command-executor.sh'])); 23 | static File windowsDesktopShortcutCreatorFile = 24 | File(combinePath([root, 'set-shortcut.ps1'])); 25 | static File windowsCommandExecutorFile = 26 | File(combinePath([root, 'windows-command-executor.ps1'])); 27 | 28 | static String get root => gpmDir.absolute.path; 29 | 30 | static void init() { 31 | if (!gpmDir.existsSync()) { 32 | gpmDir.createSync(); 33 | } 34 | if (!downloadsDir.existsSync()) { 35 | downloadsDir.createSync(); 36 | } 37 | if (!registryDir.existsSync()) { 38 | registryDir.createSync(); 39 | } 40 | if (!appsDir.existsSync()) { 41 | appsDir.createSync(); 42 | } 43 | if (Platform.isLinux) { 44 | if (!unixCommandExecutorFile.existsSync()) { 45 | unixCommandExecutorFile 46 | .writeAsStringSync(_Scripts().unixCommandExecutor, flush: true); 47 | SystemService.makeExecutable(unixCommandExecutorFile.path); 48 | } 49 | } 50 | if (Platform.isWindows) { 51 | if (!windowsDesktopShortcutCreatorFile.existsSync()) { 52 | windowsDesktopShortcutCreatorFile.writeAsStringSync( 53 | _Scripts().windowsDesktopShortcutCreator, 54 | flush: true); 55 | } 56 | if (!windowsCommandExecutorFile.existsSync()) { 57 | windowsCommandExecutorFile 58 | .writeAsStringSync(_Scripts().windowsCommandExecutor, flush: true); 59 | } 60 | } 61 | } 62 | 63 | static void cleanDownloads(_) { 64 | if (_) { 65 | if (downloadsDir.existsSync()) { 66 | final files = downloadsDir.listSync(); 67 | for (final file in files) { 68 | file.deleteSync(recursive: true); 69 | } 70 | } 71 | print('Deleted temporary downloaded installers and cloned repositories.'); 72 | terminateInstance(); 73 | } 74 | } 75 | 76 | static String toPath(ReleaseAssetEntity assetEntity) { 77 | return combinePath( 78 | [GPMStorage.root, GPMStorage.downloadStorage, assetEntity.name]); 79 | } 80 | 81 | static String toRegistryPath(String id) { 82 | final owner = parseOwnerName(id); 83 | final repo = parseRepoName(id); 84 | final ownerDir = Directory(combinePath([root, registryStorage, owner])); 85 | if (!ownerDir.existsSync()) { 86 | ownerDir.createSync(); 87 | } 88 | return combinePath([ownerDir.path, '$repo.json']); 89 | } 90 | 91 | static String toAppDirPath(String id) { 92 | final owner = parseOwnerName(id); 93 | final repo = parseRepoName(id); 94 | final ownerDir = Directory(combinePath([root, appsStorage, owner])); 95 | if (!ownerDir.existsSync()) { 96 | ownerDir.createSync(); 97 | } 98 | final repoDir = Directory(combinePath([ownerDir.path, repo])); 99 | if (!repoDir.existsSync()) { 100 | repoDir.createSync(); 101 | } 102 | return repoDir.path; 103 | } 104 | 105 | static String toAppPath(ReleaseAssetEntity asset) { 106 | final owner = asset.owner; 107 | final repo = asset.repo; 108 | final ownerDir = Directory(combinePath([root, appsStorage, owner])); 109 | if (!ownerDir.existsSync()) { 110 | ownerDir.createSync(); 111 | } 112 | final repoDir = Directory(combinePath([ownerDir.path, repo])); 113 | if (!repoDir.existsSync()) { 114 | repoDir.createSync(); 115 | } 116 | return combinePath([repoDir.path, '$repo.${getExtension(asset.name)}']); 117 | } 118 | 119 | static String toClonedRepoPath(String id) { 120 | final owner = parseOwnerName(id); 121 | final repo = parseRepoName(id); 122 | final ownerDir = Directory(combinePath([root, downloadStorage, owner])); 123 | if (!ownerDir.existsSync()) { 124 | ownerDir.createSync(); 125 | } 126 | final repoDir = Directory(combinePath([ownerDir.path, repo])); 127 | if (!repoDir.existsSync()) { 128 | repoDir.createSync(); 129 | } 130 | return combinePath([repoDir.path, "$repo.zip"]); 131 | } 132 | } 133 | 134 | class _Scripts { 135 | final unixCommandExecutor = """#!/bin/sh 136 | \$@"""; 137 | 138 | final windowsCommandExecutor = """ 139 | \$allArgs = \$PsBoundParameters.Values + \$args 140 | pwsh -Command \$allArgs 141 | exit \$Lastexitcode 142 | """; 143 | 144 | final windowsDesktopShortcutCreator = """ 145 | \$DestinationPath = \$env:DestinationPath 146 | \$SourceExe = \$env:SourceExe 147 | echo "Shortcut Path: \$DestinationPath" 148 | echo "Source Path: \$SourceExe" 149 | \$WshShell = New-Object -comObject WScript.Shell 150 | \$Shortcut = \$WshShell.CreateShortcut(\$DestinationPath) 151 | \$Shortcut.TargetPath = \$SourceExe 152 | \$Shortcut.Save() 153 | 154 | """; 155 | } 156 | -------------------------------------------------------------------------------- /lib/core/provider/compatible_asset_provider.dart: -------------------------------------------------------------------------------- 1 | import '../../constants/enums.dart'; 2 | import '../../entity/asset_entity.dart'; 3 | import '../../extras/extras.dart'; 4 | 5 | class CompatibleReleaseAssetProvider { 6 | final List<ReleaseAssetEntity> _primary; 7 | final List<ReleaseAssetEntity> _secondary; 8 | final List<ReleaseAssetEntity> _others; 9 | 10 | bool get hasPrimary => _primary.isNotEmpty; 11 | 12 | bool get hasSecondary => _secondary.isNotEmpty; 13 | 14 | bool get hasOthers => _others.isNotEmpty; 15 | 16 | List<ReleaseAssetEntity> get primary => _primary; 17 | 18 | List<ReleaseAssetEntity> get secondary => _secondary; 19 | 20 | List<ReleaseAssetEntity> get others => _others; 21 | 22 | CompatibleReleaseAssetProvider({ 23 | required List<ReleaseAssetEntity> primary, 24 | required List<ReleaseAssetEntity> secondary, 25 | required List<ReleaseAssetEntity> others, 26 | }) : _others = others, 27 | _secondary = secondary, 28 | _primary = primary; 29 | 30 | factory CompatibleReleaseAssetProvider.fromList({ 31 | required List<ReleaseAssetEntity> assets, 32 | required OS target, 33 | }) { 34 | // Getting OS Compatible Extensions 35 | final acceptableExtensions = 36 | ExtensionPriorities.getExtensionsWithPriorityOrder(target); 37 | 38 | // Finding OS Compatible Assets 39 | Map<String, List<ReleaseAssetEntity>> compatibleAssets = {}; 40 | 41 | for (final ext in acceptableExtensions) { 42 | compatibleAssets[ext] = <ReleaseAssetEntity>[]; 43 | } 44 | 45 | for (final ext in acceptableExtensions) { 46 | compatibleAssets[ext]!.addAll(_findTargetAssets(assets, ext, target)); 47 | } 48 | 49 | List<ReleaseAssetEntity> primary = []; 50 | List<ReleaseAssetEntity> secondary = []; 51 | List<ReleaseAssetEntity> others = []; 52 | 53 | if (compatibleAssets.isNotEmpty) { 54 | final primaryExtension = acceptableExtensions[0]; 55 | primary = compatibleAssets[primaryExtension]!; 56 | if (acceptableExtensions.length > 1) { 57 | // this means this is a supported os 58 | final secondaryExtension = acceptableExtensions[1]; 59 | secondary = compatibleAssets[secondaryExtension]!; 60 | if (acceptableExtensions.length > 2) { 61 | // this means this is a well-known supported os 62 | final entries = compatibleAssets.entries; 63 | for (final compatibleAsset in entries) { 64 | final ext = compatibleAsset.key; 65 | if (ext != primaryExtension && ext != secondaryExtension) { 66 | others.addAll(compatibleAsset.value); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | return CompatibleReleaseAssetProvider( 74 | primary: primary, 75 | secondary: secondary, 76 | others: others, 77 | ); 78 | } 79 | 80 | static List<ReleaseAssetEntity> _findTargetAssets( 81 | List<ReleaseAssetEntity> assets, 82 | String targetExtension, 83 | OS target, 84 | ) { 85 | List<ReleaseAssetEntity> results = []; 86 | final isUniversalTargetExtension = ExtensionPriorities 87 | .universalArtifactsExtensions 88 | .contains(targetExtension); 89 | for (final asset in assets) { 90 | final ext = getExtension(asset.name); 91 | if (ext == targetExtension) { 92 | if (isUniversalTargetExtension) { 93 | // this means this is a universal artifact 94 | // we need to check if it's compatible with the target os 95 | if (asset.isCompatibleWithOS()) { 96 | results.add(asset); 97 | } 98 | } else { 99 | results.add(asset); 100 | } 101 | } 102 | } 103 | return results; 104 | } 105 | } 106 | 107 | class ExtensionPriorities { 108 | ExtensionPriorities._(); 109 | 110 | static final universalArtifactsExtensions = [ 111 | 'zip', 112 | 'xz', 113 | 'gz', 114 | ]; 115 | 116 | static final otherLinuxArtifactsExtensions = [ 117 | 'AppImage', 118 | ...universalArtifactsExtensions, 119 | ]; 120 | 121 | static final windows = OSCompatibleExtensions(OS.windows, [ 122 | 'msi', 123 | 'exe', 124 | ...universalArtifactsExtensions, 125 | ]); 126 | static final macos = OSCompatibleExtensions(OS.macos, [ 127 | 'dmg', 128 | ...universalArtifactsExtensions, 129 | ]); 130 | static final debian = OSCompatibleExtensions(OS.debian, [ 131 | 'deb', 132 | ...otherLinuxArtifactsExtensions, 133 | ]); 134 | static final fedora = OSCompatibleExtensions(OS.fedora, [ 135 | 'rpm', 136 | ...otherLinuxArtifactsExtensions, 137 | ]); 138 | static final arch = OSCompatibleExtensions(OS.arch, [ 139 | 'zst', 140 | ...otherLinuxArtifactsExtensions, 141 | ]); 142 | static final linux = OSCompatibleExtensions(OS.linux, [ 143 | ...otherLinuxArtifactsExtensions, 144 | ]); 145 | static final unrecognized = OSCompatibleExtensions(OS.unrecognized, [ 146 | ...universalArtifactsExtensions, 147 | ]); 148 | 149 | static final List<OSCompatibleExtensions> priorities = [ 150 | windows, 151 | macos, 152 | linux, 153 | debian, 154 | fedora, 155 | arch, 156 | unrecognized, 157 | ]; 158 | 159 | static List<String> getExtensionsWithPriorityOrder(OS target) { 160 | return priorities 161 | .firstWhere((priority) => priority.os == target) 162 | .extensions; 163 | } 164 | } 165 | 166 | class OSCompatibleExtensions { 167 | final OS os; 168 | final List<String> extensions; 169 | 170 | OSCompatibleExtensions(this.os, this.extensions); 171 | } 172 | -------------------------------------------------------------------------------- /lib/core/service/system_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chalkdart/chalkstrings.dart'; 4 | import 'package:gpm/constants/constants.dart'; 5 | 6 | import '../../constants/enums.dart'; 7 | import '../../extras/extras.dart'; 8 | import '../../extras/linux_utils.dart'; 9 | import '../storage/gpm_storage.dart'; 10 | 11 | class SystemService { 12 | SystemService._(); 13 | 14 | static late final OS _os; 15 | 16 | static String get os => _os.name; 17 | 18 | static OS get osObject => _os; 19 | 20 | static bool get isKnownLinuxDistribution => 21 | KnownLinuxDistro.values.any((e) => e.name == os); 22 | 23 | static void init() { 24 | if (Platform.isWindows) { 25 | _os = OS.windows; 26 | } else if (Platform.isMacOS) { 27 | _os = OS.macos; 28 | print('[GPM] MacOS is not currently supported.'); 29 | } else if (Platform.isLinux) { 30 | // we further try to identify the host linux distribution's parent. 31 | // For any linux distro we assume that [/etc/os-release] file exist 32 | final path = '/etc/os-release'; 33 | final releaseFile = File(path); 34 | if (releaseFile.existsSync()) { 35 | final contents = releaseFile.readAsStringSync(); 36 | final data = parseLinuxSystemInfo(contents); 37 | if (data.isDebian()) { 38 | _os = OS.debian; 39 | } else if (data.isFedora()) { 40 | _os = OS.fedora; 41 | } else if (data.isArch()) { 42 | _os = OS.arch; 43 | } else { 44 | _os = OS.linux; 45 | // at this point ... 46 | // an unknown linux distro encountered 47 | // go gpm !! 48 | } 49 | } else { 50 | _os = OS.linux; 51 | // again ... 52 | // an unknown linux distro encountered 53 | // go gpm !! 54 | } 55 | } else { 56 | _os = OS.unrecognized; 57 | print("WARNING: Unrecognized Platform: ${Platform.operatingSystem}" 58 | .yellow 59 | .bold); 60 | } 61 | } 62 | 63 | static List<String> getOSKeywords() { 64 | final os = SystemService.osObject; 65 | switch (os) { 66 | case OS.windows: 67 | return OSKeywords.windows; 68 | case OS.linux: 69 | return OSKeywords.linux; 70 | case OS.macos: 71 | return OSKeywords.macos; 72 | case OS.debian: 73 | return OSKeywords.debian; 74 | case OS.fedora: 75 | return OSKeywords.fedora; 76 | case OS.arch: 77 | return OSKeywords.arch; 78 | case OS.unrecognized: 79 | return OSKeywords.unrecognized; 80 | } 81 | } 82 | 83 | static int executeSync(String cmd, [String? workingDir, bool log = true]) { 84 | int exitCode = 0; 85 | if (Platform.isWindows) { 86 | final result = Process.runSync( 87 | 'pwsh', 88 | [ 89 | '-file', 90 | GPMStorage.windowsCommandExecutorFile.path, 91 | cmd, 92 | ], 93 | workingDirectory: workingDir ?? getUserHome(), 94 | runInShell: true, 95 | environment: Platform.environment, 96 | ); 97 | exitCode = result.exitCode; 98 | if (exitCode != 0) { 99 | print(result.stdout); 100 | print(result.stderr); 101 | } 102 | } else if (Platform.isLinux) { 103 | final result = Process.runSync( 104 | GPMStorage.unixCommandExecutorFile.path, 105 | [cmd], 106 | workingDirectory: workingDir ?? getUserHome(), 107 | runInShell: true, 108 | ); 109 | exitCode = result.exitCode; 110 | if (exitCode != 0) { 111 | print(result.stdout); 112 | print(result.stderr); 113 | } 114 | } 115 | return exitCode; 116 | } 117 | 118 | static Future<int> execute(String cmd, 119 | [String? workingDir, bool log = true]) async { 120 | int exitCode = 0; 121 | if (Platform.isWindows) { 122 | final result = await Process.run( 123 | 'pwsh', 124 | [ 125 | '-file', 126 | GPMStorage.windowsCommandExecutorFile.path, 127 | cmd, 128 | ], 129 | workingDirectory: workingDir ?? getUserHome(), 130 | runInShell: true, 131 | environment: Platform.environment, 132 | ); 133 | exitCode = result.exitCode; 134 | if (exitCode != 0) { 135 | print(result.stdout); 136 | print(result.stderr); 137 | } 138 | } else if (Platform.isLinux) { 139 | final result = await Process.run( 140 | GPMStorage.unixCommandExecutorFile.path, 141 | [cmd], 142 | workingDirectory: workingDir ?? getUserHome(), 143 | runInShell: true, 144 | ); 145 | exitCode = result.exitCode; 146 | if (exitCode != 0) { 147 | print(result.stdout); 148 | print(result.stderr); 149 | } 150 | } 151 | return exitCode; 152 | } 153 | 154 | static void makeExecutable(String filePath) { 155 | final result = Process.runSync( 156 | 'chmod', 157 | ['+x', filePath], 158 | ); 159 | if (result.exitCode != 0) { 160 | print(result.stderr); 161 | } 162 | } 163 | 164 | static void addToPath(id, String path) { 165 | if (Platform.isWindows) { 166 | print("To ensure security on your windows operating system, "); 167 | print("gpm will not itself update the path variable."); 168 | print( 169 | "Please add ${path.blue.bold} to your System Environment Variable to access the installed cli program throughtout your system."); 170 | } else if (Platform.isLinux) { 171 | // identifying shell 172 | // defaults to bash 173 | final shell = Platform.environment['SHELL'] ?? '/bin/bash'; 174 | String rcFile = '.bashrc'; 175 | if (shell.contains('zsh')) { 176 | rcFile = '.zshrc'; 177 | } else if (shell.contains('fish')) { 178 | rcFile = '.fishrc'; 179 | } 180 | final file = File(combinePath([getUserHome(), rcFile])); 181 | file.writeAsStringSync( 182 | 'PATH="$path:\$PATH"', 183 | mode: FileMode.append, 184 | flush: true, 185 | ); 186 | print("Added $id to PATH, Start a new shell or run `source ~/$rcFile` to check it out."); 187 | } 188 | } 189 | 190 | /// Checks if an executable or command exists in the system 191 | /// by running if with --help flag, 192 | /// if the exitCode is 0, then the executable exists 193 | /// else we assume that the executable is not installed or not on path 194 | static bool doesExecutableExists(String executable) { 195 | if (Platform.isWindows) { 196 | try { 197 | final exitCode = SystemService.executeSync('$executable --help'); 198 | return exitCode == 0; 199 | } catch (e) { 200 | return false; 201 | } 202 | } else if (Platform.isLinux) { 203 | try { 204 | final result = Process.runSync( 205 | executable, 206 | ['--help'], 207 | ); 208 | return result.exitCode == 0; 209 | } catch (e) { 210 | return false; 211 | } 212 | } else { 213 | print("GPM doesn't supports your operating system, yet."); 214 | return false; 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![gpm-banner](https://github.com/user-attachments/assets/e1de1053-26d3-44be-9fcf-09348e4a4bd2) 2 | 3 | <div align="center"> 4 | <h3>Generic Package Manager</h3> 5 | <p> 6 | Directly install applications from their GitHub repository with out-of-the-box support for updates<br>using gpm, the package manager with the superpowers of a cross platform build tool. 7 | </p> 8 | <a href="https://github.com/generic-package-manager/gpm/blob/main/docs/EXAMPLES.md"><img src="https://img.shields.io/badge/Usage-Examples-pink?style=for-the-badge" alt="Examples"/></a> 9 | </div> 10 | 11 | ## Getting Started 12 | 13 | Hi! there, follow this guide to install `gpm` cli on your system. 14 | At the end of this guide, you will find additional resources related to cli usage. 15 | 16 | **Let's start**. 17 | ## Platform Compatibility 18 | Here are the platforms currently supported by gpm: 19 | - 🔥👌 Windows OS 20 | - 🔥👌 Linux (Any) 21 | - 🪵🚫 Mac OS (Not Supported, yet) 22 | 23 | ## Installation Methods 24 | There are many ways to install `gpm`. 25 | 26 | Choose the one which suits you the best: 27 | - (1) Install directly from terminal 28 | - (2) Install from releases 29 | - (3) Install gpm using gpm (yeah, it's possible) 30 | - (4) Build gpm using gpm 31 | 32 | ### (1) ⚡ Install directly from terminal 33 | To use this method, make sure you have the requirements installed as per your os: 34 | 35 | **For Windows** 36 | 37 | **Requirements**: 38 | *pwsh (PowerShell)* on Windows OS 39 | ```shell 40 | . { iwr -useb https://raw.githubusercontent.com/generic-package-manager/gpm/main/pyro/windows/install.ps1 } | iex 41 | ``` 42 | 43 | **For Linux** 44 | 45 | **Requirements**: *curl* on Linux 46 | ```shell 47 | curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/generic-package-manager/gpm/main/pyro/linux/install.sh | bash 48 | ``` 49 | 50 | ### (2) ⚡ Install from releases 51 | This method is pretty easy but its not recommended as you won't recieve any update on `gpm cli` and you need to manually update your installation, in this method all you need to do is head over to [releases](https://github.com/generic-package-manager/gpm/releases/latest), and download the binary for your operating system. 52 | 53 | Direct download links are provided below: 54 | 55 | **For Windows**: Click to get [`gpm.exe`](https://github.com/generic-package-manager/gpm/releases/latest/download/gpm.exe) and [`gpm-binary-replacer.exe`](https://github.com/generic-package-manager/gpm/releases/latest/download/gpm-binary-replacer.exe) 56 | 57 | **For Linux** : Click to get [`gpm.AppImage`](https://github.com/generic-package-manager/gpm/releases/latest/download/gpm.AppImage) 58 | 59 | (**Important:** Rename **`gpm.AppImage`** to **`gpm`**) 60 | 61 | After downloading the binary(s), place them at a location like **`~/tools/gpm`** and add the location to your system environment path variable. 62 | 63 | ### (3) ⚡ Install gpm using gpm 64 | You can install gpm using gpm itself: 65 | 66 | Step 1: Download the gpm binary for your operating system by following the steps in (2) method of installation. 67 | 68 | Step 2: Run the downloaded binary as: 69 | 70 | **For Windows** 71 | 72 | ```shell 73 | ./gpm.exe --build generic-package-manager/gpm --yes 74 | ``` 75 | 76 | **For Linux** 77 | 78 | ```shell 79 | ./gpm --build generic-package-manager/gpm --yes 80 | ``` 81 | 82 | ### (4) ⚡ Build gpm using gpm 83 | `gpm` is written in dart, thus, it compiles to native code for the best performance. 84 | 85 | For compiling gpm, You will require dart version **3.2.6** or above. 86 | 87 | Additionally on Windows, you need to have pwsh (PowerShell) version **7.4.0**. 88 | 89 | There are two ways to build gpm: 90 | 91 | **Traditional**: 92 | 93 | ```shell 94 | git clone https://github.com/generic-package-manager/gpm.git 95 | cd gpm 96 | # for windows 97 | dart compile exe -o gpm.exe --target-os windows bin\gpm.dart 98 | dart compile exe -o gpm-binary-replacer.exe --target-os windows bin\gpm_binary_replacer.dart 99 | # for linux 100 | dart compile exe -o gpm --target-os linux bin/gpm.dart 101 | ``` 102 | 103 | after completing the above steps, you will get the compiled `gpm` binary. 104 | 105 | **Self Build and Install**: 106 | 107 | You can even use gpm to build itself locally. 108 | ```shell 109 | git clone https://github.com/generic-package-manager/gpm.git 110 | cd gpm 111 | ./gpm-dev --build-locally generic-package-manager/gpm 112 | ``` 113 | 114 | > if you are encountering any issues in installing gpm, please create an issue [here](https://github.com/generic-package-manager/gpm/issues/new). 115 | 116 | ## Features 117 | 118 | GPM brings the following: 119 | 120 | - Any open source repository on GitHub is available to install as a package. 121 | 122 | - As soon as you publish your repository on GitHub, its available for installing with gpm-cli. 123 | 124 | - You can rollback the installed updates on any package. 125 | - `gpm --rollback obs-studio` 126 | 127 | - You can even install a specific tag/version of any repository. 128 | - `gpm --install rufus --tag v4.0` 129 | 130 | - You can control which apps should receive updates 131 | - `gpm --lock app-fleet` // lock updates 132 | 133 | - `gpm --unlock app-fleet` // unlock updates 134 | 135 | - You can update any package individually or update all of them at once. 136 | - `gpm --update gpm` or `gpm --upgrade` 137 | 138 | - You can even build a repository from source. 139 | - `gpm --build generic-package-manager/gpm` for more information on this see [building from source with gpm](/docs/BUILD_FROM_SOURCE.md). 140 | 141 | - You can even install a specific commit. 142 | - `gpm --build generic-package-manager/gpm --commit XXXXXXX`, where XXXXXXX is at least a 7 char commit hash. 143 | 144 | - You can even build a cloned repository locally. 145 | - `gpm --build-locally generic-package-manager/gpm` 146 | 147 | 148 | ## Usage 149 | 150 | To install a repository run 151 | 152 | ```shell 153 | gpm --install username/repo 154 | ``` 155 | 156 | Want a specific version installed? 157 | 158 | ```shell 159 | gpm --install username/repo --tag <version> 160 | ``` 161 | 162 | I wanna build from source !! 163 | 164 | ```shell 165 | gpm --build username/repo 166 | ``` 167 | 168 | Not the latest commit 169 | 170 | ```shell 171 | gpm --build username/repo --commit <hash> 172 | ``` 173 | 174 | What about updates? 175 | 176 | ```shell 177 | gpm --update username/repo 178 | ``` 179 | 180 | How to update them all? 181 | 182 | ```shell 183 | gpm --upgrade 184 | ``` 185 | 186 | Oops! I don't need update on this package 187 | 188 | ```shell 189 | gpm --lock username/repo 190 | ``` 191 | 192 | What!!! It worked better before the update 193 | 194 | ```shell 195 | gpm --roll-back username/repo 196 | ``` 197 | 198 | But My repository is private 199 | 200 | ```shell 201 | gpm --install username/repository --token <your_github_token> 202 | ``` 203 | 204 | ## Build from source 205 | 206 | #### Requirements 207 | On Windows: **dart ^3.2.6 and pwsh ^7.4.0** 208 | 209 | On Linux: **dart ^3.2.6** 210 | 211 | ```shell 212 | # clone the repo 213 | git clone https://github.com/generic-package-manager/gpm 214 | cd gpm 215 | ``` 216 | 217 | ```shell 218 | # use gpm to build from source 219 | # for windows 220 | ./gpm-dev.bat --build-locally generic-package-manager/gpm 221 | # for linux 222 | ./gpm-dev --build-locally generic-package-manager/gpm 223 | ``` 224 | 225 | -------------------------------------------------------------------------------- /lib/core/service/api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:gpm/gpm_cli.dart'; 4 | import 'package:http/http.dart' as http; 5 | import 'package:yaml/yaml.dart'; 6 | 7 | import '../../entity/asset_entity.dart'; 8 | import '../../extras/extras.dart'; 9 | import '../logging/logger.dart'; 10 | import 'package_registry_service.dart'; 11 | 12 | class APIService { 13 | APIService._(); 14 | 15 | static Future<Map<String, String>> searchOnGitHub(String repo) async { 16 | final url = 17 | 'https://api.github.com/search/repositories?q=$repo&per_page=10&sort=stars'; 18 | try { 19 | final response = await http.get( 20 | Uri.parse(url), 21 | headers: getGitHubAPIHeaders(), 22 | ); 23 | if (response.statusCode == 200) { 24 | final body = response.body; 25 | final map = jsonDecode(body); 26 | final items = map['items']; 27 | if (items != null) { 28 | final resultMap = <String, String>{}; 29 | for (final item in items) { 30 | resultMap.putIfAbsent( 31 | item['full_name'], () => item['stargazers_count'].toString()); 32 | } 33 | return resultMap; 34 | } 35 | } 36 | } catch (e) { 37 | debugPrint(e); 38 | print("Unable to connect with $url"); 39 | debugPrint( 40 | "Either the app id format is incorrect or it is some network error", 41 | tag: 'SERVICE', 42 | ); 43 | } 44 | return <String, String>{}; 45 | } 46 | 47 | static Future<String?> getLatestCommit(String id) async { 48 | final url = 'https://api.github.com/repos/$id/commits'; 49 | try { 50 | final response = await http.get( 51 | Uri.parse(url), 52 | headers: getGitHubAPIHeaders(), 53 | ); 54 | final body = response.body; 55 | final map = jsonDecode(body); 56 | final firstCommit = map[0]; 57 | if (firstCommit != null) { 58 | final sha = firstCommit['sha']; 59 | if (sha != null) { 60 | return sha.toString().substring(0, 7); 61 | } 62 | } 63 | } catch (e) { 64 | debugPrint(e); 65 | print("Unable to connect with $url"); 66 | debugPrint( 67 | "Either the app id format is incorrect or it is some network error", 68 | tag: 'SERVICE', 69 | ); 70 | } 71 | return null; 72 | } 73 | 74 | static Future<dynamic> getGPMSpecification(String id) async { 75 | final url = 'https://api.github.com/repos/$id/contents/gpm.yaml'; 76 | try { 77 | if (commitHash != null) { 78 | final url = 'https://github.com/$id/raw/$commitHash/gpm.yaml'; 79 | final response = await http.get( 80 | Uri.parse(url), 81 | headers: getGitHubAPIHeaders(), 82 | ); 83 | if (response.statusCode == 200) { 84 | final body = response.body; 85 | return loadYaml(body); 86 | } else { 87 | print( 88 | '[WARNING] ${parseRepoName(id)} does not support gpm in its $commitHash commit'); 89 | print('[WARNING] We\'ll instead use the latest build instructions.'); 90 | } 91 | } 92 | final response = await http.get( 93 | Uri.parse(url), 94 | headers: getGitHubAPIHeaders(), 95 | ); 96 | final body = response.body; 97 | final map = jsonDecode(body); 98 | if (map['size'] != null && map['size'] > 0) { 99 | final contentWithLineFeedChar = map['content'].toString(); 100 | String content = ''; 101 | final chars = contentWithLineFeedChar.codeUnits; 102 | for (final ch in chars) { 103 | if (ch != 10) { 104 | content += String.fromCharCode(ch); 105 | } 106 | } 107 | final baseDecoded = base64Decode(content); 108 | final contentString = String.fromCharCodes(baseDecoded); 109 | return loadYaml(contentString); 110 | } 111 | } catch (e) { 112 | debugPrint(e); 113 | print("Unable to connect with $url"); 114 | debugPrint( 115 | "Either the app id format is incorrect or it is some network error", 116 | tag: 'SERVICE', 117 | ); 118 | } 119 | return null; 120 | } 121 | 122 | static Future<String?> getLatestTag(String id) async { 123 | final url = 'https://api.github.com/repos/$id/releases/latest'; 124 | try { 125 | final response = await http.get( 126 | Uri.parse(url), 127 | headers: getGitHubAPIHeaders(), 128 | ); 129 | final body = response.body; 130 | final map = jsonDecode(body); 131 | final tag = map['tag_name']; 132 | return tag; 133 | } catch (e) { 134 | debugPrint(e); 135 | print("Unable to connect with $url"); 136 | debugPrint( 137 | "Either the app id format is incorrect or it is some network error", 138 | tag: 'SERVICE', 139 | ); 140 | } 141 | return null; 142 | } 143 | 144 | static Future<List<ReleaseAssetEntity>> fetchAssets(String id) async { 145 | List<ReleaseAssetEntity> assets = []; 146 | var prefix = targetTag == 'latest' ? '' : 'tags/'; 147 | var url = 'https://api.github.com/repos/$id/releases/$prefix$targetTag'; 148 | try { 149 | final response = await http.get( 150 | Uri.parse(url), 151 | headers: getGitHubAPIHeaders(), 152 | ); 153 | final body = response.body; 154 | final map = jsonDecode(body); 155 | final assetsList = map['assets'] ?? []; 156 | final versions = []; 157 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 158 | final installedRelease = PackageRegistryService.getReleaseObject(id); 159 | versions.addAll(installedRelease.versions); 160 | } 161 | final license = await getRepoLicense(id); 162 | for (final assetData in assetsList) { 163 | assets.add(ReleaseAssetEntity.fromMap( 164 | parseOwnerName(id), 165 | parseRepoName(id), 166 | map['tag_name'] ?? 'none', 167 | assetData 168 | ..['explicit_version'] = targetTag != 'latest' 169 | ..['license'] = license 170 | ..['versions'] = versions, 171 | )); 172 | } 173 | } catch (e) { 174 | print(e); 175 | print("Unable to connect with $url"); 176 | if (verbose) { 177 | print( 178 | "Either the app id format is incorrect or it is some network error"); 179 | } 180 | } 181 | return assets; 182 | } 183 | 184 | static Future<bool> doesRepoExists(String id) async { 185 | final url = 'https://api.github.com/repos/$id'; 186 | try { 187 | final response = await http.get( 188 | Uri.parse(url), 189 | headers: getGitHubAPIHeaders(), 190 | ); 191 | return response.statusCode == 200; 192 | } catch (e) { 193 | print("Unable to connect with $url"); 194 | debugPrint( 195 | "Either the app id format is incorrect or it is some network error", 196 | tag: 'SERVICE'); 197 | return false; 198 | } 199 | } 200 | 201 | static Future<String> getRepoLicense(String id) async { 202 | final url = 'https://api.github.com/repos/$id'; 203 | try { 204 | final response = await http.get( 205 | Uri.parse(url), 206 | headers: getGitHubAPIHeaders(), 207 | ); 208 | final license = jsonDecode(response.body)['license'] ?? jsonDecode('{}'); 209 | return license['name'] ?? "Unknown"; 210 | } catch (e) { 211 | return "Unknown"; 212 | } 213 | } 214 | } 215 | 216 | Map<String, String>? getGitHubAPIHeaders() { 217 | if (token != null && token!.isNotEmpty) { 218 | return { 219 | 'Authorization': 'Bearer $token', 220 | }; 221 | } 222 | return null; 223 | } 224 | -------------------------------------------------------------------------------- /lib/core/service/package_disintegration_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chalkdart/chalkstrings.dart'; 4 | import 'package:gpm/gpm_cli.dart'; 5 | 6 | import '../../constants/enums.dart'; 7 | import '../../extras/extras.dart'; 8 | import '../../extras/linux_utils.dart'; 9 | import '../../extras/windows_utils.dart'; 10 | import '../storage/gpm_storage.dart'; 11 | import 'package_registry_service.dart'; 12 | import 'system_service.dart'; 13 | 14 | class PackageDisintegrationService { 15 | PackageDisintegrationService._(); 16 | 17 | static void disintegrate(String id) { 18 | int code = 0; 19 | switch (SystemService.osObject) { 20 | case OS.windows: 21 | code = _disintegrateWindowsPackage(id); 22 | break; 23 | case OS.linux: 24 | code = _disintegateSourceCommon(id); 25 | break; 26 | case OS.debian: 27 | code = _disintegrateDebianPackage(id); 28 | break; 29 | case OS.fedora: 30 | code = _disintegrateFedoraPackage(id); 31 | break; 32 | case OS.arch: 33 | code = _disintegrateArchPackage(id); 34 | break; 35 | case OS.macos: 36 | break; 37 | case OS.unrecognized: 38 | break; 39 | } 40 | if (code == 0) { 41 | // this means the disintegration was successful 42 | // now we have to remove the trace of the installed package 43 | // using the package register 44 | PackageRegistryService.remove(id: id); 45 | } 46 | terminateInstance(exitCode: code); 47 | } 48 | 49 | static int _disintegrateWindowsPackage(String id) { 50 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 51 | final asset = PackageRegistryService.getReleaseObject(id); 52 | if (asset.isSetup()) { 53 | // handle msi 54 | print('${parseRepoName(id)} was installed via an unknown installer.' 55 | .magenta); 56 | print('Head over to Installed Apps from Windows Settings to remove it.' 57 | .blue); 58 | print('Removing the app from gpm\'s registry ...'); 59 | print('the app will not be getting updates now.'); 60 | print('You can safely uninstall it.'); 61 | } else { 62 | // handle exe and others 63 | // deleting the desktop shortcut 64 | print("Removing Desktop Shortcut ...".blue.bold); 65 | WindowsUtils.deleteDesktopShortcut(asset.repo); 66 | // removing the app data from ~/.gpm/apps 67 | print("Removing App Data ...".blue.bold); 68 | final app = File(GPMStorage.toAppPath(asset)).parent; 69 | if (app.existsSync()) { 70 | app.deleteSync(recursive: true); 71 | } 72 | print("Successfully removed $id".green.bold); 73 | } 74 | return 0; 75 | } else { 76 | return _disintegateSourceCommon(id); 77 | } 78 | } 79 | 80 | static int _disintegrateDebianPackage(String id) { 81 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 82 | final asset = PackageRegistryService.getReleaseObject(id); 83 | if (asset.isSetup()) { 84 | print('Removing $id installed via dpkg ...'); 85 | // handle deb using dpkg -r 86 | final fullName = asset.name; 87 | // by conventions the package name is separated by an underscore `_` character 88 | final packageName = fullName.substring(0, fullName.indexOf('_')); 89 | final exitCode = SystemService.executeSync('sudo dpkg -r $packageName'); 90 | if (exitCode != 0) { 91 | print('Failed to remove $packageName from your system.'); 92 | } else { 93 | print("Successfully removed $id".green.bold); 94 | } 95 | } else { 96 | // handle .AppImage and others 97 | // deleting the desktop shortcut 98 | print("Removing Desktop Shortcut ...".magenta.bold); 99 | LinuxUtils.deleteDesktopShortcut(asset.repo); 100 | // removing the app data from ~/.gpm/apps 101 | print("Removing App Data ...".magenta.bold); 102 | final app = File(GPMStorage.toAppPath(asset)).parent; 103 | if (app.existsSync()) { 104 | app.deleteSync(recursive: true); 105 | } 106 | print("Successfully removed $id".green.bold); 107 | } 108 | return 0; 109 | } else { 110 | return _disintegateSourceCommon(id); 111 | } 112 | } 113 | 114 | static int _disintegrateFedoraPackage(String id) { 115 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 116 | final asset = PackageRegistryService.getReleaseObject(id); 117 | if (asset.isSetup()) { 118 | print('Removing $id installed via dnf ...'); 119 | // handle rpm using dnf remove 120 | final fullName = asset.name; 121 | // by conventions the package name is separated by an underscore `_` character 122 | final packageName = fullName.substring(0, fullName.indexOf('_')); 123 | final exitCode = 124 | SystemService.executeSync('sudo dnf remove $packageName'); 125 | if (exitCode != 0) { 126 | print('Failed to remove $packageName from your system.'); 127 | } else { 128 | print("Successfully removed $id".green.bold); 129 | } 130 | } else { 131 | // handle .AppImage and others 132 | // deleting the desktop shortcut 133 | print("Removing Desktop Shortcut ...".magenta.bold); 134 | LinuxUtils.deleteDesktopShortcut(asset.repo); 135 | // removing the app data from ~/.gpm/apps 136 | print("Removing App Data ...".magenta.bold); 137 | final app = File(GPMStorage.toAppPath(asset)).parent; 138 | if (app.existsSync()) { 139 | app.deleteSync(recursive: true); 140 | } 141 | print("Successfully removed $id".green.bold); 142 | } 143 | return 0; 144 | } else { 145 | return _disintegateSourceCommon(id); 146 | } 147 | } 148 | 149 | static int _disintegrateArchPackage(String id) { 150 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 151 | final asset = PackageRegistryService.getReleaseObject(id); 152 | if (asset.isSetup()) { 153 | print('Removing $id installed via pacman ...'); 154 | // handle zst using pacman -R 155 | final fullName = asset.name; 156 | // by conventions the package name is separated by an underscore `_` character 157 | final packageName = fullName.substring(0, fullName.indexOf('_')); 158 | final exitCode = 159 | SystemService.executeSync('sudo pacman -R $packageName'); 160 | if (exitCode != 0) { 161 | print('Failed to remove $packageName from your system.'); 162 | } else { 163 | print("Successfully removed $id".green.bold); 164 | } 165 | } else { 166 | // handle .AppImage and others 167 | // deleting the desktop shortcut 168 | print("Removing Desktop Shortcut ...".magenta.bold); 169 | LinuxUtils.deleteDesktopShortcut(asset.repo); 170 | // removing the app data from ~/.gpm/apps 171 | print("Removing App Data ...".magenta.bold); 172 | final app = File(GPMStorage.toAppPath(asset)).parent; 173 | if (app.existsSync()) { 174 | app.deleteSync(recursive: true); 175 | } 176 | print("Successfully removed $id".green.bold); 177 | } 178 | return 0; 179 | } else { 180 | return _disintegateSourceCommon(id); 181 | } 182 | } 183 | 184 | static int _disintegateSourceCommon(String id) { 185 | try { 186 | // deleting source binary data 187 | print("Removing App Data ...".magenta.bold); 188 | final sourcePath = GPMStorage.toAppDirPath(id); 189 | final dir = Directory(sourcePath); 190 | if (dir.existsSync()) { 191 | dir.deleteSync(recursive: true); 192 | } 193 | // cleaning desktop shortcut 194 | LinuxUtils.deleteDesktopShortcut(parseRepoName(id)); 195 | print("Successfully removed $id".green.bold); 196 | } catch (e) { 197 | print("[DSC] Clean up failed."); 198 | print('Failed to remove $id from your system.'); 199 | return 1; 200 | } 201 | return 0; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /lib/gpm_cli.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:chalkdart/chalkstrings.dart'; 6 | import 'package:gpm/core/logging/exit_codes.dart'; 7 | import 'package:gpm/core/provider/compatible_asset_provider.dart'; 8 | import 'package:gpm/core/service/package_registry_service.dart'; 9 | import 'package:gpm/core/service/package_service.dart'; 10 | import 'package:gpm/core/service/system_service.dart'; 11 | import 'package:gpm/core/service/upgrade_service.dart'; 12 | import 'package:gpm/core/storage/gpm_instance_manager.dart'; 13 | import 'package:gpm/core/storage/gpm_storage.dart'; 14 | import 'package:gpm/extras/extras.dart'; 15 | 16 | final instanceID = generateGPMIsolateID(); 17 | int instanceExitCode = ExitCodes.fine; 18 | ReceivePort currentIsolateReceivePort = ReceivePort(instanceID); 19 | 20 | void terminateInstance({exitCode}) { 21 | instanceExitCode = exitCode ?? instanceExitCode; 22 | currentIsolateReceivePort.sendPort.send('exit'); 23 | } 24 | 25 | final String _message = """ 26 | 27 | ._____ ._______ ._____.___ 28 | :_ ___\\ : ____ |: | 29 | | |___| : || \\ / | 30 | | / || |___|| |\\/ | 31 | |. __ ||___| |___| | | 32 | :/ |. | |___| 33 | : :/ 34 | : 35 | 36 | ${"Generic Package Manager".bold} 37 | """; 38 | 39 | /// cli version 40 | const String version = '1.0.1'; 41 | 42 | // testing compatibility with gui 43 | 44 | /// verbose flag 45 | bool verbose = false; 46 | 47 | // additional cli options 48 | String? targetTag = 'latest'; 49 | String? mode = 'release'; 50 | String? listMode; 51 | String? listType = 'all'; 52 | String? commitHash; 53 | String? token; // used to access private repositories 54 | String? option; 55 | 56 | ArgParser buildParser() { 57 | return ArgParser() 58 | ..addSeparator('Options & Flags:') 59 | ..addFlag( 60 | 'yes', 61 | negatable: false, 62 | help: 63 | 'When passed, gpm will not ask for confirmation before any operation.', 64 | callback: (value) => yesToAll = value, 65 | ) 66 | ..addOption( 67 | 'option', 68 | help: 69 | 'Should be an integer, used to automatically select the release target without asking the user.', 70 | valueHelp: "1, 2, 3 ...", 71 | callback: (value) => option = value, 72 | ) 73 | ..addSeparator("") 74 | ..addOption( 75 | 'list-mode', 76 | allowed: ['release', 'source'], 77 | help: 'List apps installed via specific mode.', 78 | callback: (value) => listMode = value, 79 | ) 80 | ..addOption( 81 | 'list-type', 82 | allowed: ['primary', 'secondary', 'others', 'all'], 83 | defaultsTo: 'all', 84 | help: 85 | 'List apps installed via specific types.\nHere\'s the priority list for your operating system: ${ExtensionPriorities.getExtensionsWithPriorityOrder(SystemService.osObject).join(', ')}.', 86 | callback: (value) => listType = value, 87 | ) 88 | ..addFlag( 89 | 'list', 90 | negatable: false, 91 | help: 'List all apps with installed versions.', 92 | callback: PackageRegistryService.listInstalledApps, 93 | ) 94 | ..addSeparator("") 95 | ..addOption( 96 | 'tag', 97 | defaultsTo: 'latest', 98 | help: 99 | 'Specify the release tag you want to install along with --install option.', 100 | callback: (value) => targetTag = value, 101 | ) 102 | ..addOption( 103 | 'commit', 104 | abbr: 'c', 105 | help: 106 | 'Specify the commit hash you want to build from source along with --build option.', 107 | callback: (value) => commitHash = value, 108 | ) 109 | ..addOption( 110 | 'token', 111 | help: 112 | 'Specify your access token for fetching private repos, defaults to GITHUB_TOKEN Environment Variable.', 113 | callback: (value) { 114 | token = value; 115 | if (token == null || token!.isEmpty) { 116 | token = Platform.environment['GITHUB_TOKEN']; 117 | } 118 | }, 119 | ) 120 | ..addSeparator("") 121 | ..addOption( 122 | 'lock', 123 | help: 'Pauses update for an app.', 124 | callback: (value) => PackageRegistryService.lockUpdates(id: value), 125 | ) 126 | ..addOption( 127 | 'unlock', 128 | help: 'Resumes update for an app.', 129 | callback: (value) => PackageRegistryService.unlockUpdates(id: value), 130 | ) 131 | ..addSeparator("") 132 | ..addOption( 133 | 'install', 134 | abbr: 'i', 135 | help: 'Install an app from a user\'s repo, updates if already installed.', 136 | callback: (value) => 137 | PackageService.handleInstall(value, explicitCall: true), 138 | ) 139 | ..addOption( 140 | 'build', 141 | abbr: 'b', 142 | help: 'Build an app from source.', 143 | callback: (value) { 144 | if (value != null && value.isNotEmpty) { 145 | mode = 'source'; 146 | PackageService.handleInstall(value, explicitCall: true); 147 | } 148 | }, 149 | ) 150 | ..addOption( 151 | 'build-locally', 152 | help: 'Build from source using the local `gpm.yaml` specification.', 153 | callback: (value) => PackageService.buildLocally(value), 154 | ) 155 | ..addOption( 156 | 'remove', 157 | abbr: 'r', 158 | help: 'Remove an installed app.', 159 | callback: PackageService.handleRemove, 160 | ) 161 | ..addOption( 162 | 'update', 163 | abbr: 'u', 164 | help: 'Updates an already installed app.', 165 | callback: (value) => 166 | PackageService.handleUpdate(value, explicitCall: true), 167 | ) 168 | ..addSeparator("") 169 | ..addOption( 170 | 'roll-back', 171 | help: 'Rollback an app to its previously installed release version.', 172 | callback: (value) => 173 | PackageService.handleRollback(value, explicitCall: true), 174 | ) 175 | ..addOption( 176 | 'roll-forward', 177 | help: 'Invert of `--rollback`.', 178 | callback: (value) => 179 | PackageService.handleRollforward(value, explicitCall: true), 180 | ) 181 | ..addSeparator("") 182 | ..addFlag( 183 | 'clean', 184 | negatable: false, 185 | help: 'Removes any left over or temporary downloaded files.', 186 | callback: GPMStorage.cleanDownloads, 187 | ) 188 | ..addFlag( 189 | 'upgrade', 190 | negatable: false, 191 | help: 'Updates all apps to their latest versions.', 192 | callback: PackageService.handleUpgrade, 193 | ) 194 | ..addFlag( 195 | 'check-for-updates', 196 | negatable: false, 197 | help: 198 | 'Checks for updates and generates a update-data.json file at ~/.gpm.', 199 | callback: (value) { 200 | if (value) { 201 | UpgradeService.checkUpdates(explicitCall: true); 202 | } 203 | }, 204 | ) 205 | ..addFlag( 206 | 'verbose', 207 | abbr: 'v', 208 | negatable: false, 209 | help: 'Show additional command output.', 210 | ) 211 | ..addFlag( 212 | 'version', 213 | negatable: false, 214 | help: 'Print the tool version.', 215 | ) 216 | ..addFlag( 217 | 'help', 218 | abbr: 'h', 219 | negatable: false, 220 | help: 'Print this usage information.', 221 | ); 222 | } 223 | 224 | void printUsage(ArgParser argParser) { 225 | print('Usage: gpm <options> [arguments]\n'); 226 | print(argParser.usage); 227 | } 228 | 229 | void checkSyntax(List<String> arguments) { 230 | if (arguments.isNotEmpty && !arguments[0].startsWith('-')) { 231 | print("Incorrect usage. Use --help for more information."); 232 | print("May be you mean: gpm --${arguments[0]}"); 233 | } 234 | } 235 | 236 | void run(List<String> arguments) { 237 | // Lock.find(); 238 | checkSyntax(arguments); 239 | SystemService.init(); 240 | GPMStorage.init(); 241 | final ArgParser argParser = buildParser(); 242 | try { 243 | final ArgResults results = argParser.parse(arguments); 244 | 245 | if (arguments.isEmpty || results.wasParsed('help')) { 246 | printUsage(argParser); 247 | terminateInstance(); 248 | } 249 | GPMInstanceManager.helpTheHelper(); 250 | if (results.wasParsed('version')) { 251 | stdout.write(_message); 252 | print('gpm cli version: $version'); 253 | terminateInstance(); 254 | } 255 | if (results.wasParsed('verbose')) { 256 | verbose = true; 257 | if (results.arguments.length == 1) { 258 | terminateInstance(); 259 | } 260 | } 261 | } on FormatException catch (e) { 262 | // Print usage information if an invalid argument was provided. 263 | print(e.message); 264 | print(''); 265 | printUsage(argParser); 266 | terminateInstance(exitCode: ExitCodes.error); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /lib/core/service/package_registry_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chalkdart/chalkstrings.dart'; 4 | import 'package:cli_table/cli_table.dart'; 5 | import 'package:gpm/core/logging/exit_codes.dart'; 6 | import 'package:gpm/core/service/package_service.dart'; 7 | import 'package:path/path.dart'; 8 | 9 | import '../../gpm_cli.dart'; 10 | import '../../entity/asset_entity.dart'; 11 | import '../../entity/source_entity.dart'; 12 | import '../storage/gpm_storage.dart'; 13 | import '../storage/omegaui/json_configurator.dart'; 14 | import '../storage/storage_keys.dart'; 15 | 16 | class PackageRegistryService { 17 | PackageRegistryService._(); 18 | 19 | static void lockUpdates({ 20 | required String? id, 21 | }) async { 22 | id = await PackageService.findFullQualifiedID(id, searchOnGitHub: false); 23 | if (id != null) { 24 | final configPath = GPMStorage.toRegistryPath(id); 25 | bool exists = File(configPath).existsSync(); 26 | if (exists) { 27 | final config = JsonConfigurator(configPath: configPath); 28 | if (config.get('explicit_version') ?? false) { 29 | print("$id is already locked."); 30 | } else { 31 | config.put('explicit_version', true); 32 | print("$id will not receive updates from now on.".red); 33 | } 34 | terminateInstance(); 35 | } else { 36 | print("$id is not installed."); 37 | print("run the following to install it:"); 38 | print("gpm --install $id".dim.bold); 39 | print("after it's installed, you can lock it."); 40 | terminateInstance(exitCode: ExitCodes.unavailable); 41 | } 42 | } 43 | } 44 | 45 | static void unlockUpdates({ 46 | required String? id, 47 | }) async { 48 | id = await PackageService.findFullQualifiedID(id, searchOnGitHub: false); 49 | if (id != null) { 50 | final configPath = GPMStorage.toRegistryPath(id); 51 | bool exists = File(configPath).existsSync(); 52 | if (exists) { 53 | final config = JsonConfigurator(configPath: configPath); 54 | if (config.get('explicit_version') ?? false) { 55 | config.put('explicit_version', false); 56 | print("$id will receive updates from now on.".blue); 57 | } else { 58 | print("$id is already unlocked."); 59 | } 60 | terminateInstance(); 61 | } else { 62 | print("$id is not installed."); 63 | print("run the following to install it:"); 64 | print("gpm --install $id".dim.bold); 65 | terminateInstance(exitCode: ExitCodes.unavailable); 66 | } 67 | } 68 | } 69 | 70 | static void registerRelease({ 71 | required ReleaseAssetEntity asset, 72 | }) async { 73 | final configPath = GPMStorage.toRegistryPath(asset.appID); 74 | bool isNewInstall = !File(configPath).existsSync(); 75 | final config = JsonConfigurator(configPath: configPath); 76 | if (!asset.versions.contains(asset.tag)) { 77 | asset.versions.add(asset.tag); 78 | } 79 | config.addAll(asset.toMap()); 80 | config.put(StorageKeys.mode, 'release'); 81 | if (isNewInstall) { 82 | config.put(StorageKeys.installedAt, DateTime.now().toString()); 83 | } else { 84 | config.put(StorageKeys.updatedAt, DateTime.now().toString()); 85 | } 86 | } 87 | 88 | static void remove({required String id}) { 89 | final registryFile = File(GPMStorage.toRegistryPath(id)); 90 | if (registryFile.existsSync()) { 91 | registryFile.deleteSync(); 92 | } 93 | } 94 | 95 | static bool isPackageInstalled(String id) { 96 | final registryFile = File(GPMStorage.toRegistryPath(id)); 97 | return registryFile.existsSync(); 98 | } 99 | 100 | static bool isPackageInstalledViaReleaseMode(String id) { 101 | final configPath = GPMStorage.toRegistryPath(id); 102 | final config = JsonConfigurator(configPath: configPath); 103 | final mode = config.get(StorageKeys.mode); 104 | if (mode == null) { 105 | config.deleteSync(); 106 | } 107 | return mode == 'release'; 108 | } 109 | 110 | static bool isPackageInstalledViaSourceMode(String id) { 111 | final configPath = GPMStorage.toRegistryPath(id); 112 | final config = JsonConfigurator(configPath: configPath); 113 | final mode = config.get(StorageKeys.mode); 114 | if (mode == null) { 115 | config.deleteSync(); 116 | } 117 | return mode == 'source'; 118 | } 119 | 120 | static ReleaseAssetEntity getReleaseObject(String id) { 121 | final configPath = GPMStorage.toRegistryPath(id); 122 | final config = JsonConfigurator(configPath: configPath); 123 | return ReleaseAssetEntity.fromMap( 124 | config.get('owner'), 125 | config.get('repo'), 126 | config.get('tag'), 127 | config.config, 128 | ); 129 | } 130 | 131 | static SourceEntity getSourceObject(String id) { 132 | final configPath = GPMStorage.toRegistryPath(id); 133 | final config = JsonConfigurator(configPath: configPath); 134 | return SourceEntity.fromMap( 135 | config.config, 136 | ); 137 | } 138 | 139 | static List<String> getInstalledApps() { 140 | List<String> appIDs = []; 141 | final appRegistries = GPMStorage.registryDir.listSync(recursive: true); 142 | appRegistries 143 | .removeWhere((e) => !e.path.endsWith('.json')); // filtering json files 144 | for (final registry in appRegistries) { 145 | final owner = basename(registry.parent.path); 146 | final repo = basenameWithoutExtension(registry.path); 147 | final id = '$owner/$repo'; 148 | appIDs.add(id); 149 | } 150 | return appIDs; 151 | } 152 | 153 | static List<ReleaseAssetEntity> getInstalledReleases() { 154 | final installedReleases = <ReleaseAssetEntity>[]; 155 | final appIDs = getInstalledApps(); 156 | for (final id in appIDs) { 157 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 158 | installedReleases.add(PackageRegistryService.getReleaseObject(id)); 159 | } 160 | } 161 | return installedReleases; 162 | } 163 | 164 | static List<SourceEntity> getInstalledSources() { 165 | final installedSources = <SourceEntity>[]; 166 | final appIDs = getInstalledApps(); 167 | for (final id in appIDs) { 168 | if (PackageRegistryService.isPackageInstalledViaSourceMode(id)) { 169 | installedSources.add(PackageRegistryService.getSourceObject(id)); 170 | } 171 | } 172 | return installedSources; 173 | } 174 | 175 | static void listInstalledApps(_) { 176 | if (_) { 177 | if (listMode == null || listMode == 'release') { 178 | // gettings all apps 179 | final installedReleases = getInstalledReleases(); 180 | installedReleases.sort((a, b) => a.appID.compareTo(b.appID)); 181 | // filtering mode 182 | final none = listType == 'all'; 183 | // printing information in tabular form 184 | final table = Table( 185 | header: [ 186 | 'Package Name'.blue.bold, 187 | 'Installed Version'.blue.bold, 188 | if (none) 'Type'.blue.bold, 189 | ], 190 | columnWidths: [30, 20], 191 | ); 192 | for (final asset in installedReleases) { 193 | if (none || asset.type == listType) { 194 | table.add([ 195 | '${asset.owner}/${asset.repo}', 196 | asset.tag, 197 | if (none) asset.type, 198 | ]); 199 | } 200 | } 201 | if (table.isNotEmpty) { 202 | print('Apps Installed via release mode'); 203 | print(table.toString()); 204 | } else { 205 | print( 206 | 'Couldn\'t find any packages installed via gpm (release mode).'); 207 | } 208 | } 209 | if (listMode == null || listMode == 'source') { 210 | // gettings all apps 211 | final installedSources = getInstalledSources(); 212 | installedSources.sort((a, b) => a.appID.compareTo(b.appID)); 213 | // printing information in tabular form 214 | final table = Table( 215 | header: [ 216 | 'Package Name'.blue.bold, 217 | 'Installed Commit Hash'.blue.bold, 218 | ], 219 | columnWidths: [30, 35], 220 | ); 221 | for (final asset in installedSources) { 222 | table.add([ 223 | '${asset.owner}/${asset.repo}', 224 | asset.commitHash, 225 | ]); 226 | } 227 | if (table.isNotEmpty) { 228 | print('Apps Installed via source mode'); 229 | print(table.toString()); 230 | } else { 231 | print('Couldn\'t find any packages installed via gpm (source mode).'); 232 | } 233 | } 234 | terminateInstance(); 235 | } 236 | } 237 | 238 | static void registerSource(SourceEntity source) { 239 | final configPath = 240 | GPMStorage.toRegistryPath('${source.owner}/${source.repo}'); 241 | bool isNewInstall = !File(configPath).existsSync(); 242 | final config = JsonConfigurator(configPath: configPath); 243 | config.addAll(source.toMap()); 244 | config.put(StorageKeys.mode, 'source'); 245 | config.add(StorageKeys.versions, source.commitHash); 246 | if (isNewInstall) { 247 | config.put(StorageKeys.installedAt, DateTime.now().toString()); 248 | } else { 249 | config.put(StorageKeys.updatedAt, DateTime.now().toString()); 250 | } 251 | } 252 | 253 | static List<String> getSourceCommits(id) { 254 | final configPath = GPMStorage.toRegistryPath(id); 255 | final config = JsonConfigurator(configPath: configPath); 256 | return List<String>.from(config.get('versions') ?? <String>[]); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /lib/core/service/upgrade_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:chalkdart/chalkstrings.dart'; 2 | import 'package:cli_spin/cli_spin.dart'; 3 | import 'package:cli_table/cli_table.dart'; 4 | import 'package:gpm/core/storage/gpm_storage.dart'; 5 | import 'package:gpm/core/storage/omegaui/json_configurator.dart'; 6 | import 'package:gpm/gpm_cli.dart'; 7 | 8 | import '../../entity/asset_entity.dart'; 9 | import '../../entity/source_entity.dart'; 10 | import '../../extras/extras.dart'; 11 | import 'api_service.dart'; 12 | import 'package_registry_service.dart'; 13 | import 'package_service.dart'; 14 | 15 | class UpgradeService { 16 | UpgradeService._(); 17 | 18 | static final _installedReleases = 19 | PackageRegistryService.getInstalledReleases(); 20 | static final _installedSources = PackageRegistryService.getInstalledSources(); 21 | 22 | // map of release packages with latest versions 23 | static final Map<String, String> _versionsData = {}; 24 | 25 | // map of source packages with latest versions 26 | static final Map<String, String> _sourceUpdateData = {}; 27 | 28 | static Future<List<String>> checkUpdates({bool explicitCall = false}) async { 29 | // a map to store fetched release data 30 | Map<String, List<ReleaseAssetEntity>> releaseUpdateData = {}; 31 | if (_installedReleases.isNotEmpty) { 32 | print( 33 | 'Checking updates for ${_installedReleases.length} packages installed via release mode ...'); 34 | 35 | // a future to store the api fetch list 36 | List<Future> futures = []; 37 | 38 | // creating a cli spinner 39 | final spinner = CliSpin( 40 | text: 'Fetching release data ...', 41 | spinner: CliSpinners.pipe, 42 | ).start(); 43 | 44 | // start time 45 | final startTime = DateTime.now(); 46 | 47 | // internal function to check 48 | // - availability of repo 49 | // - availability of assets 50 | Future<void> queryRepo(String id) async { 51 | if (await APIService.doesRepoExists(id)) { 52 | releaseUpdateData[id] = await APIService.fetchAssets(id); 53 | } 54 | } 55 | 56 | // initiating api service 57 | for (final app in _installedReleases) { 58 | futures.add(queryRepo(app.appID)); 59 | } 60 | 61 | await Future.wait(futures); 62 | 63 | spinner.stopAndPersist( 64 | text: 65 | '➜ ${'[OK]'.green.bold} Fetch Completed, ${'[took ${formatTime(DateTime.now().difference(startTime))}]'}.', 66 | ); 67 | } 68 | 69 | // a function to check if an app can be updated 70 | bool isReleaseUpdatable(ReleaseAssetEntity installedRelease) { 71 | if (installedRelease.explicitVersion) { 72 | // this means that the user explicitly installed 73 | // a specific tag, so, we omit updates for such repos 74 | return false; 75 | } 76 | final id = '${installedRelease.owner}/${installedRelease.repo}'; 77 | if (releaseUpdateData.containsKey(id)) { 78 | final releases = releaseUpdateData[id]!; 79 | if (releases.isNotEmpty) { 80 | final latestTag = releases.first.tag; 81 | final updateAvailable = installedRelease.tag != latestTag; 82 | if (updateAvailable) { 83 | _versionsData[id] = latestTag; 84 | } 85 | return updateAvailable; 86 | } 87 | } 88 | return false; 89 | } 90 | 91 | // next, we compare and find which apps need an update 92 | final updatableApps = <String>[]; 93 | for (final installedRelease in _installedReleases) { 94 | if (isReleaseUpdatable(installedRelease)) { 95 | updatableApps.add(installedRelease.appID); 96 | } 97 | } 98 | 99 | // Now, we check the apps installed via source mode 100 | final futures = <Future>[]; 101 | Future<void> fetchLatestCommit(id) async { 102 | if (await APIService.doesRepoExists(id)) { 103 | final commit = await APIService.getLatestCommit(id); 104 | if (commit != null) { 105 | _sourceUpdateData[id] = commit; 106 | } 107 | } 108 | } 109 | 110 | if (_installedSources.isNotEmpty) { 111 | print( 112 | 'Checking updates for ${_installedSources.length} packages installed via source mode ...'); 113 | 114 | // start time 115 | final startTime = DateTime.now(); 116 | 117 | // creating a cli spinner 118 | final spinner = CliSpin( 119 | text: 'Fetching commit data ...', 120 | spinner: CliSpinners.pipe, 121 | ).start(); 122 | 123 | for (final source in _installedSources) { 124 | futures.add(fetchLatestCommit(source.appID)); 125 | } 126 | 127 | await Future.wait(futures); 128 | 129 | spinner.stopAndPersist( 130 | text: 131 | '➜ ${'[OK]'.green.bold} Fetch Completed, ${'[took ${formatTime(DateTime.now().difference(startTime))}]'}.', 132 | ); 133 | 134 | // a function to check if an app can be updated 135 | bool isSourceUpdatable(SourceEntity source) { 136 | if (source.explicitVersion) { 137 | // this means that the user explicitly marked an app as 138 | // not-suitable for update 139 | // so, we omit updates for such repos 140 | return false; 141 | } 142 | final id = source.appID; 143 | if (_sourceUpdateData.containsKey(id)) { 144 | return _sourceUpdateData[id] != source.commitHash.substring(0, 7); 145 | } 146 | return false; 147 | } 148 | 149 | for (final installedSource in _installedSources) { 150 | if (isSourceUpdatable(installedSource)) { 151 | updatableApps.add(installedSource.appID); 152 | } 153 | } 154 | } 155 | 156 | if (updatableApps.isNotEmpty) { 157 | if (explicitCall) { 158 | print('${updatableApps.length} packages can be updated ...'); 159 | } 160 | 161 | final updatableReleases = <ReleaseAssetEntity>[]; 162 | 163 | for (final release in _installedReleases) { 164 | if (updatableApps.contains(release.appID)) { 165 | updatableReleases.add(release); 166 | } 167 | } 168 | 169 | final updatableSources = <SourceEntity>[]; 170 | 171 | for (final source in _installedSources) { 172 | if (updatableApps.contains(source.appID)) { 173 | updatableSources.add(source); 174 | } 175 | } 176 | 177 | // a function to get installed tag by package id 178 | String getTag(String id) { 179 | for (final installedRelease in _installedReleases) { 180 | final appID = installedRelease.appID; 181 | if (appID == id) { 182 | return installedRelease.tag; 183 | } 184 | } 185 | return 'unknown'; 186 | } 187 | 188 | // a function to get installed commit by package id 189 | String getCommitHash(String id) { 190 | for (final installSource in _installedSources) { 191 | final appID = installSource.appID; 192 | if (appID == id) { 193 | return installSource.commitHash; 194 | } 195 | } 196 | return 'unknown'; 197 | } 198 | 199 | // json file to be written for update references 200 | final updateConfig = JsonConfigurator( 201 | configPath: combinePath([GPMStorage.root, 'update-data.json'])); 202 | updateConfig.deleteSync(); 203 | 204 | if (updatableReleases.isNotEmpty) { 205 | // printing releases update information in tabular form 206 | final table = Table( 207 | header: [ 208 | 'Package Name'.blue.bold, 209 | 'Installed Version'.blue.bold, 210 | 'Available Version'.blue.bold 211 | ], 212 | columnWidths: [30, 20, 20], 213 | ); 214 | for (final app in updatableReleases) { 215 | final tag = getTag(app.appID); 216 | final latest = _versionsData[app.appID].toString(); 217 | table.add([ 218 | app.appID, 219 | tag, 220 | latest.blue.bold, 221 | ]); 222 | if (explicitCall) { 223 | updateConfig.add('releases', { 224 | 'package': app.appID, 225 | 'current': tag, 226 | 'latest': latest, 227 | }); 228 | } 229 | } 230 | print(table.toString()); 231 | } 232 | 233 | if (updatableSources.isNotEmpty) { 234 | // printing releases update information in tabular form 235 | final table = Table( 236 | header: [ 237 | 'Package Name'.blue.bold, 238 | 'Installed Commit'.blue.bold, 239 | 'Available Commit'.blue.bold 240 | ], 241 | columnWidths: [30, 20, 20], 242 | ); 243 | for (final app in updatableSources) { 244 | final commitHash = getCommitHash(app.appID); 245 | final latest = _sourceUpdateData[app.appID].toString(); 246 | table.add([ 247 | app.appID, 248 | commitHash, 249 | latest.blue.bold, 250 | ]); 251 | if (explicitCall) { 252 | updateConfig.add('sources', { 253 | 'package': app.appID, 254 | 'current': commitHash.substring(0, 7), 255 | 'latest': latest, 256 | }); 257 | } 258 | } 259 | print(table.toString()); 260 | } 261 | } else { 262 | if (explicitCall) { 263 | print('All your apps are already up-to-date.'); 264 | } 265 | } 266 | if (explicitCall) { 267 | terminateInstance(); 268 | } 269 | return updatableApps; 270 | } 271 | 272 | static Future<void> doUpgrade(List<String> appIDs) async { 273 | final updatableApps = await checkUpdates(); 274 | // now, we got the repos which can be updated 275 | if (updatableApps.isNotEmpty) { 276 | print('${updatableApps.length} packages can be updated ...'); 277 | if (!yes('Do you wish to upgrade? (y/N): ')) { 278 | terminateInstance(); 279 | return; 280 | } 281 | 282 | // putting yesToAll to true 283 | yesToAll = true; 284 | 285 | // updating apps one by one 286 | int count = 0; 287 | for (final id in updatableApps) { 288 | print('➜ Updating $id ...'.magenta); 289 | bool result = await PackageService.handlePackageUpdate( 290 | id, 291 | parseRepoName(id), 292 | false, 293 | showUpdateLogs: false, 294 | ); 295 | if (result) { 296 | count++; 297 | } 298 | } 299 | if (count == updatableApps.length) { 300 | print('All your apps are now up-to-date.'); 301 | terminateInstance(); 302 | } else { 303 | int failed = updatableApps.length - count; 304 | print('Failed to update $failed apps.'.red.bold); 305 | if (count != 0) { 306 | print('Updated $count to their latest versions.'.green.bold.dim); 307 | } 308 | terminateInstance(exitCode: failed); 309 | } 310 | } else { 311 | print('All your apps are already up-to-date.'); 312 | terminateInstance(); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /lib/core/service/package_integration_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chalkdart/chalkstrings.dart'; 4 | import 'package:gpm/core/service/api_service.dart'; 5 | 6 | import '../../gpm_cli.dart'; 7 | import '../../constants/enums.dart'; 8 | import '../../entity/asset_entity.dart'; 9 | import '../../extras/extras.dart'; 10 | import '../../extras/linux_utils.dart'; 11 | import '../../extras/windows_utils.dart'; 12 | import '../storage/gpm_storage.dart'; 13 | import 'package_registry_service.dart'; 14 | import 'system_service.dart'; 15 | 16 | class PackageIntegrationService { 17 | PackageIntegrationService._(); 18 | 19 | static Future<void> integrate( 20 | ReleaseAssetEntity asset, String packagePath, bool explicitCall) async { 21 | int code = 0; 22 | switch (SystemService.osObject) { 23 | case OS.windows: 24 | code = await _integrateWindowsPackage(asset, packagePath); 25 | break; 26 | case OS.linux: 27 | code = await _integrateLinuxCommon(asset, packagePath); 28 | break; 29 | case OS.macos: 30 | break; 31 | case OS.debian: 32 | code = await _integrateDebianPackage(asset, packagePath); 33 | break; 34 | case OS.fedora: 35 | code = await _integrateFedoraPackage(asset, packagePath); 36 | break; 37 | case OS.arch: 38 | code = await _integrateArchPackage(asset, packagePath); 39 | break; 40 | case OS.unrecognized: 41 | break; 42 | } 43 | if (code == 0) { 44 | // this means the integration was successful 45 | // now we have to keep track of the installed package 46 | // using the package register 47 | PackageRegistryService.registerRelease(asset: asset); 48 | } 49 | 50 | if (explicitCall) terminateInstance(exitCode: code); 51 | } 52 | 53 | static Future<int> _integrateWindowsPackage( 54 | ReleaseAssetEntity asset, String packagePath) async { 55 | if (asset.isSetup()) { 56 | // BUG (But its a feature): ms-powertoys bypasses this check somehow 57 | print("Installing package ...".blue.bold); 58 | int exitCode = SystemService.executeSync(packagePath); 59 | if (exitCode == 0) { 60 | print("Successfully Installed Package".green.bold); 61 | } else { 62 | print("Failed to install package".red.bold); 63 | } 64 | return exitCode; 65 | } 66 | if (verbose) { 67 | print( 68 | "Looks like we have downloaded an app ${"(not a setup)".magenta.bold}."); 69 | print("Don't worry, GPM can still manage updates for it."); 70 | print("You can launch it or pin it where you want."); 71 | print("Just do not move it away from ~/.gpm/apps.".yellow.dim); 72 | print( 73 | "If you do so, GPM would think you deleted the app and it will not be updated anymore." 74 | .yellow 75 | .dim); 76 | } 77 | 78 | // moving the app to a secure location 79 | // to prevent deletion from 'gpm --clean' command 80 | movePath(packagePath, (packagePath = GPMStorage.toAppPath(asset))); 81 | print("Source: ${packagePath.blue.bold}"); 82 | 83 | if (asset.type == 'secondary') { 84 | print("Creating a desktop shortcut ...".blue.bold); 85 | final code = WindowsUtils.createDesktopShortcut(asset.repo, packagePath); 86 | if (code != 0) { 87 | print("Sorry, we couldn't create a desktop shortcut for this app." 88 | .magenta 89 | .bold); 90 | print('Although, the download was successful.'); 91 | if (yes('Would you like to open it in explorer? [y/N]: ')) { 92 | WindowsUtils.openInExlorer(File(packagePath).parent.path); 93 | } 94 | } 95 | print("Successfully installed ${asset.repo}.".green.bold); 96 | } else if (asset.type == 'others') { 97 | // on windows the other targets are actually compressed files, 98 | // extracting downloaded target ... 99 | if (verbose) { 100 | print('Seems like the downloaded asset is a compressed file,'); 101 | print('GPM may not create a desktop shortcut for such an app.'); 102 | } 103 | print('Extracting package ...'); 104 | bool result = await extract(packagePath, File(packagePath).parent.path); 105 | if (result) { 106 | print('Extraction Completed ...'); 107 | // trying to identify the app binary to create a desktop shortcut 108 | final appDir = File(packagePath).parent; 109 | final appBinary = File(combinePath([appDir.path, '${asset.repo}.exe'])); 110 | if (appBinary.existsSync()) { 111 | print("Creating a desktop shortcut ...".blue.bold); 112 | final code = 113 | WindowsUtils.createDesktopShortcut(asset.repo, appBinary.path); 114 | if (code != 0) { 115 | print("Sorry, we couldn't create a desktop shortcut for this app." 116 | .magenta 117 | .bold); 118 | print('Although, the download was successful.'); 119 | if (yes('Would you like to open it in explorer? [y/N]: ')) { 120 | WindowsUtils.openInExlorer(appDir.path); 121 | } 122 | } 123 | } 124 | // removing compressed archive 125 | File(packagePath).deleteSync(); 126 | print("Successfully installed ${asset.repo}.".green.bold); 127 | } else { 128 | print('A problem occurred while extracting ${packagePath.blue}'); 129 | return 1; 130 | } 131 | } 132 | return 0; 133 | } 134 | 135 | static Future<int> _integrateDebianPackage( 136 | ReleaseAssetEntity asset, String packagePath) async { 137 | if (asset.isSetup()) { 138 | // this means the asset is actually a .deb package 139 | // and we need to install it using dpkg 140 | print("Installing package via dpkg ...".blue.bold); 141 | final exitCode = SystemService.executeSync('sudo dpkg -i $packagePath'); 142 | if (exitCode == 0) { 143 | print("Successfully Installed Package via dpkg".green.bold); 144 | } else { 145 | print("Failed to install package via dpkg".red.bold); 146 | } 147 | return exitCode; 148 | } 149 | return await _integrateLinuxCommon(asset, packagePath); 150 | } 151 | 152 | static Future<int> _integrateArchPackage( 153 | ReleaseAssetEntity asset, String packagePath) async { 154 | if (asset.isSetup()) { 155 | // this means the asset is actually a .zst package 156 | // and we need to install it using dpkg 157 | print("Installing package via pacman ...".blue.bold); 158 | final exitCode = SystemService.executeSync('sudo pacman -U $packagePath'); 159 | if (exitCode == 0) { 160 | print("Successfully Installed Package via pacman".green.bold); 161 | } else { 162 | print("Failed to install package via pacman".red.bold); 163 | } 164 | return exitCode; 165 | } 166 | return await _integrateLinuxCommon(asset, packagePath); 167 | } 168 | 169 | static Future<int> _integrateFedoraPackage( 170 | ReleaseAssetEntity asset, String packagePath) async { 171 | if (asset.isSetup()) { 172 | // this means the asset is actually a .rpm package 173 | // and we need to install it using dnf 174 | print("Installing package via pacman ...".blue.bold); 175 | final exitCode = 176 | SystemService.executeSync('sudo dnf install $packagePath'); 177 | if (exitCode == 0) { 178 | print("Successfully Installed Package via dnf".green.bold); 179 | } else { 180 | print("Failed to install package via dnf".red.bold); 181 | } 182 | return exitCode; 183 | } 184 | return await _integrateLinuxCommon(asset, packagePath); 185 | } 186 | 187 | static Future<int> _integrateLinuxCommon( 188 | ReleaseAssetEntity asset, String packagePath) async { 189 | // moving the app to a secure location 190 | // to prevent deletion from 'gpm --clean' command 191 | movePath(packagePath, (packagePath = GPMStorage.toAppPath(asset))); 192 | 193 | if (asset.type == 'secondary') { 194 | // handle .AppImage 195 | // We sure have downloaded the binary and the meta data 196 | // but we are unsure about the app icon 197 | // treat .AppImage as .exe on Windows 198 | // When creating a desktop shortcut on windows, the platform 199 | // itself identifies the app icon from the binary (.exe) 200 | // but in case of any linux distros, they cannot do the same 201 | // so in case, that, a repo host an .AppImage in releases 202 | // then, it should have a `gpm.yaml` at root 203 | // with the `icon` property set to the url of the app 204 | // such that, we can download the icon later and provide the install xp 205 | // just like an app store or else we use the gpm icon 206 | 207 | packagePath = await _renameRelease(asset.appID, packagePath); 208 | print("Source: ${packagePath.blue.bold}"); 209 | 210 | if (verbose) { 211 | print( 212 | "Looks like we have downloaded an app ${"(not a setup)".magenta.bold}."); 213 | print("Don't worry, GPM can still manage updates for it."); 214 | print("You can launch it or pin it where you want."); 215 | print("Just do not move it away from ~/.gpm/apps.".yellow.dim); 216 | print( 217 | "If you do so, GPM would think you deleted the app and it will not be updated anymore." 218 | .yellow 219 | .dim); 220 | } 221 | 222 | // we need to make sure that the binary is executable 223 | SystemService.makeExecutable(packagePath); 224 | 225 | print("Creating a desktop shortcut ...".blue.bold); 226 | final code = 227 | await LinuxUtils.createDesktopShortcut(asset.appID, packagePath); 228 | if (code != 0) { 229 | print("Sorry, we couldn't create a desktop shortcut for this app." 230 | .magenta 231 | .bold); 232 | print('Although, the download was successful.'); 233 | LinuxUtils.openInFiles(File(packagePath).parent.path); 234 | } 235 | print("Successfully installed ${asset.repo}.".green.bold); 236 | } else if (asset.type == 'others') { 237 | print("Source: ${packagePath.blue.bold}"); 238 | // extracting downloaded target ... 239 | if (verbose) { 240 | print('Seems like the downloaded asset is a compressed file,'); 241 | print('GPM may not create a desktop shortcut for such an app.'); 242 | } 243 | print('Extracting package ...'); 244 | bool result = await extract(packagePath, File(packagePath).parent.path); 245 | if (result) { 246 | print('Extraction Completed ...'); 247 | 248 | // trying to identify the app binary to create a desktop shortcut 249 | final appDir = File(packagePath).parent; 250 | final appBinary = File(combinePath([appDir.path, (asset.repo)])); 251 | 252 | // we need to make sure that the binary is executable 253 | SystemService.makeExecutable(appBinary.path); 254 | 255 | if (appBinary.existsSync()) { 256 | print("Creating a desktop shortcut ...".blue.bold); 257 | final code = await LinuxUtils.createDesktopShortcut( 258 | asset.appID, appBinary.path); 259 | if (code != 0) { 260 | print("Sorry, we couldn't create a desktop shortcut for this app." 261 | .magenta 262 | .bold); 263 | print('Although, the download was successful.'); 264 | LinuxUtils.openInFiles(appDir.path); 265 | } 266 | } 267 | // removing compressed archive 268 | File(packagePath).deleteSync(); 269 | print("Successfully installed ${asset.repo}.".green.bold); 270 | } else { 271 | print('A problem occurred while extracting ${packagePath.blue}'); 272 | return 1; 273 | } 274 | } 275 | return 0; 276 | } 277 | 278 | static Future<String> _renameRelease(String id, String packagePath) async { 279 | final spec = await APIService.getGPMSpecification(id); 280 | if (spec != null) { 281 | final releases = spec['releases']; 282 | if (releases != null) { 283 | dynamic target = releases[SystemService.os]; 284 | if (target == null) { 285 | if (SystemService.isKnownLinuxDistribution) { 286 | target = releases['linux']; 287 | } 288 | } 289 | if (target != null) { 290 | final destination = target['secondary']['renameTo']; 291 | if (destination != null) { 292 | final file = File(packagePath); 293 | final parent = file.parent; 294 | final targetPath = "${parent.path}/$destination"; 295 | file.renameSync(targetPath); 296 | packagePath = targetPath; 297 | } 298 | } 299 | } 300 | } 301 | return packagePath; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /lib/core/service/build_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:chalkdart/chalkstrings.dart'; 5 | import 'package:chalkdart/chalkstrings_x11.dart'; 6 | import 'package:cli_spin/cli_spin.dart'; 7 | import 'package:gpm/core/logging/exit_codes.dart'; 8 | import 'package:gpm/core/service/api_service.dart'; 9 | import 'package:gpm/core/storage/gpm_instance_manager.dart'; 10 | import 'package:gpm/extras/version_utils.dart'; 11 | import 'package:gpm/gpm_cli.dart' as cli; 12 | import 'package:gpm/gpm_cli.dart'; 13 | import 'package:path/path.dart'; 14 | 15 | import '../../entity/source_entity.dart'; 16 | import '../../extras/extras.dart'; 17 | import '../../extras/linux_utils.dart'; 18 | import '../../extras/windows_utils.dart'; 19 | import '../provider/build_data_provider.dart'; 20 | import '../storage/gpm_storage.dart'; 21 | import 'download_service.dart'; 22 | import 'package_registry_service.dart'; 23 | import 'system_service.dart'; 24 | 25 | class BuildService { 26 | BuildService._(); 27 | 28 | static Future<bool> handleBuildFromSource( 29 | String id, 30 | String repo, 31 | dynamic spec, { 32 | bool isLocalBuild = false, 33 | }) async { 34 | print('Checking platform compatibility ...'); 35 | bool buildSuccess = false; // flag storing result of operation 36 | final provider = BuildDataProvider.fromMap(spec); 37 | if (!provider.isHostSupported()) { 38 | print( 39 | '${id.blue.bold} doesn\'t supports building from source on ${SystemService.os.magenta.bold.dim} (your operating system).' 40 | .red 41 | .bold); 42 | } else { 43 | print('Package Type: ${provider.type.blue.bold}'); 44 | final platformBuildData = provider.getTargetPlatformBuildInstructions(); 45 | bool hasSatisfiedDependencies = true; 46 | // Checking dependencies ... 47 | if (platformBuildData.hasDependencies) { 48 | print('Verifying Dependencies ...'); 49 | for (final dependency in platformBuildData.dependencies) { 50 | stdout.write( 51 | 'Finding ${dependency.executable.blue.bold} ${dependency.version} ... '); 52 | final exists = 53 | SystemService.doesExecutableExists(dependency.executable); 54 | if (exists) { 55 | if (dependency.hasVersion) { 56 | final version = dependency.version; 57 | if (dependency.isVersionSatisfied()) { 58 | stdout.writeln('${'found!.'.bold} '); 59 | } else { 60 | stdout.writeln( 61 | '${'[ERROR] requires version $version'.red.bold} '); 62 | final installedVersion = 63 | getVersionString(dependency.executable); 64 | if (installedVersion != null) { 65 | stdout.writeln('[Installed Version] $installedVersion'); 66 | } 67 | if (dependency.hasHelp) { 68 | print('➜ [HELP] ${dependency.help.blue}'); 69 | } 70 | hasSatisfiedDependencies = false; 71 | } 72 | } else { 73 | stdout.writeln('${'found!.'.bold} '); 74 | } 75 | } else { 76 | stdout.writeln('${'[NO]'.red.bold} '); 77 | bool dependencyResolved = false; 78 | if (dependency.hasInstallCommand) { 79 | print( 80 | "Attempting to resolve dependency: ${dependency.executable.blue} ..."); 81 | print( 82 | "➜ ${"[EXECUTING]".aliceBlue.dim} ${dependency.installCommand}"); 83 | SystemService.executeSync(dependency.installCommand); 84 | if (!SystemService.doesExecutableExists(dependency.executable)) { 85 | print( 86 | '➜ ${"[ERROR]".red.dim} Failed to install dependency. Please see help below.'); 87 | hasSatisfiedDependencies = false; 88 | } else { 89 | print("➜ ${"[SUCCESS]".green.dim} Dependency resolved."); 90 | dependencyResolved = true; 91 | } 92 | } 93 | if (!dependencyResolved) { 94 | if (dependency.hasHelp) { 95 | print('➜ ${"[HELP]".yellow.dim} ${dependency.help.blue}'); 96 | } else { 97 | print( 98 | 'You need to install [${dependency.executable.blue}] manually.' 99 | .red); 100 | } 101 | } 102 | hasSatisfiedDependencies = dependencyResolved; 103 | } 104 | } 105 | // Starting repo cloning ... 106 | if (hasSatisfiedDependencies) { 107 | if (isLocalBuild) { 108 | buildSuccess = await _buildFromSource(provider, platformBuildData, 109 | id, Directory.current.absolute.path, isLocalBuild); 110 | } else { 111 | final target = repo; 112 | String generateText(int progress) { 113 | return "Cloning $target from GitHub ... "; 114 | } 115 | 116 | final spinner = CliSpin( 117 | text: generateText(0), 118 | spinner: CliSpinners.pipe, 119 | ).start(); 120 | 121 | // removing older clones if any 122 | deleteDir(combinePath( 123 | [GPMStorage.downloadsDir.path, parseOwnerName(id), repo])); 124 | 125 | final path = GPMStorage.toClonedRepoPath(id); 126 | await DownloadService.download( 127 | url: cli.commitHash != null 128 | ? 'https://github.com/omegaui/gpm/archive/${cli.commitHash}.zip' 129 | : 'https://api.github.com/repos/$id/zipball', 130 | path: path, 131 | onProgress: (_) {}, 132 | onComplete: (path) async { 133 | spinner.stopAndPersist( 134 | text: '➜ ${'[OK]'.green.bold} Clone Completed.', 135 | ); 136 | 137 | // Extracting tarball 138 | stdout.write('Extracting zipball ... '); 139 | bool success = await extract(path, File(path).parent.path); 140 | if (success) { 141 | stdout.writeln('[SUCCESS]'.green); 142 | // finding extraction root path 143 | final rootDir = Directory(combinePath([ 144 | GPMStorage.downloadsDir.path, 145 | parseOwnerName(id), 146 | repo 147 | ])); 148 | final extractionDirPath = rootDir 149 | .listSync() 150 | .where((e) => FileSystemEntity.isDirectorySync(e.path)) 151 | .first 152 | .path; 153 | // removing zipball 154 | File(path).deleteSync(); 155 | buildSuccess = await _buildFromSource( 156 | provider, platformBuildData, id, extractionDirPath); 157 | } else { 158 | stdout.writeln('[FAILED]'.red); 159 | } 160 | }, 161 | onError: () { 162 | spinner.fail('Downloaded Failed.'.red); 163 | terminateInstance(exitCode: ExitCodes.error); 164 | }, 165 | ); 166 | } 167 | } else { 168 | print( 169 | 'Please install all the dependencies required to build from source' 170 | .red); 171 | } 172 | } 173 | } 174 | return buildSuccess; 175 | } 176 | 177 | static Future<bool> _buildFromSource( 178 | BuildDataProvider provider, 179 | PlatformBuildData platformBuildData, 180 | String id, 181 | String root, [ 182 | bool isLocalBuild = false, 183 | ]) async { 184 | if (platformBuildData.note.isNotEmpty) { 185 | print('NOTE: ${platformBuildData.note.blue.bold}'); 186 | } 187 | print('>> Starting build from source ...'.bold); 188 | final steps = platformBuildData.steps; 189 | bool failed = false; 190 | 191 | // executing steps asynchronously 192 | for (int index = 0; index < steps.length && !failed; index++) { 193 | final step = steps[index]; 194 | // starting spinner 195 | final spinner = CliSpin( 196 | text: "${"[STEP ${index + 1} / ${steps.length}]".yellow} ${step.name}", 197 | spinner: CliSpinners.pipe, 198 | color: CliSpinnerColor.yellow, 199 | ).start(); 200 | 201 | await Future(() async { 202 | final exitCode = await step.executeAsync(root); 203 | if (exitCode == 0) { 204 | spinner.stopAndPersist( 205 | text: '➜ ${'[DONE]'.green.bold} ${step.name}.', 206 | ); 207 | } else { 208 | if (step.ignoreError) { 209 | spinner.stopAndPersist( 210 | text: '➜ ${'[WARNING]'.gold.bold} ${step.name}.', 211 | ); 212 | } else { 213 | spinner.stopAndPersist( 214 | text: '➜ ${'[FAILED]'.red.bold} ${step.name}.', 215 | ); 216 | failed = true; 217 | } 218 | } 219 | }); 220 | } 221 | if (failed) { 222 | print('Couldn\'t install package due to build errors.'); 223 | return false; 224 | } else { 225 | print('Build Completed Successfully.'); 226 | print('Creating App Structure ...'); 227 | final owner = parseOwnerName(id); 228 | final repo = parseRepoName(id); 229 | 230 | // here comes the use of [appData] file list 231 | final appData = platformBuildData.appData; 232 | 233 | // parsing commit hash from root path 234 | final rootName = basename(root); 235 | 236 | // basename example: omegaui-archy-358a957 237 | // commit-hash: 358a957 238 | final commitHash = isLocalBuild 239 | ? "local_build" 240 | : rootName.substring(rootName.lastIndexOf('-') + 1); 241 | final sourceEntity = SourceEntity( 242 | owner: owner, 243 | repo: repo, 244 | commitHash: commitHash, 245 | license: await APIService.getRepoLicense(id), 246 | installedAt: DateTime.now(), 247 | explicitVersion: cli.commitHash != null, 248 | ); 249 | 250 | // moving binaries to [apps] 251 | final appPath = GPMStorage.toAppDirPath(id); 252 | final isGPMUpdatingItselfOnWindowsOS = Platform.isWindows && 253 | PackageRegistryService.isPackageInstalled(id) && 254 | owner == 'omegaui' && 255 | repo == 'gpm'; 256 | for (String path in appData) { 257 | // moving binaries to [apps] 258 | // making sure that gpm is not handling its own update 259 | final buildPath = combinePath([root, path]); 260 | if (isGPMUpdatingItselfOnWindowsOS) { 261 | path = ".$path"; 262 | } 263 | final binaryPath = combinePath([appPath, path]); 264 | movePath( 265 | buildPath, 266 | binaryPath, 267 | ); 268 | } 269 | 270 | if (isGPMUpdatingItselfOnWindowsOS) { 271 | await GPMInstanceManager.spawnBinaryReplacer(); 272 | } 273 | 274 | // removing residuals 275 | if (!isLocalBuild) { 276 | print('Removing Residuals ...'); 277 | Directory(root).deleteSync(recursive: true); 278 | } 279 | print('Adding app to registry ...'); 280 | // creating a registry entry 281 | PackageRegistryService.registerSource(sourceEntity); 282 | 283 | // handling package type 284 | if (provider.type == 'cli') { 285 | print('Checking system environment ...'); 286 | if (!SystemService.doesExecutableExists(platformBuildData.executable)) { 287 | // notify user about adding this cli to path 288 | SystemService.addToPath(id, appPath); 289 | } 290 | print("Successfully installed $repo.".green.bold); 291 | } else { 292 | print("Creating a desktop shortcut ...".blue.bold); 293 | final packagePath = 294 | combinePath([appPath, platformBuildData.executable]); 295 | if (Platform.isWindows) { 296 | final code = WindowsUtils.createDesktopShortcut(repo, packagePath); 297 | if (code != 0) { 298 | print("Sorry, we couldn't create a desktop shortcut for this app." 299 | .magenta 300 | .bold); 301 | print('Although, the download was successful.'); 302 | if (yes('Would you like to open it in explorer? [y/N]: ')) { 303 | WindowsUtils.openInExlorer(File(packagePath).parent.path); 304 | } 305 | } 306 | } else { 307 | final code = 308 | await LinuxUtils.createDesktopShortcut(id, packagePath); 309 | if (code != 0) { 310 | print("Sorry, we couldn't create a desktop shortcut for this app." 311 | .magenta 312 | .bold); 313 | print('Although, the download was successful.'); 314 | LinuxUtils.openInFiles(File(packagePath).parent.path); 315 | } 316 | } 317 | print("Successfully installed $repo.".green.bold); 318 | } 319 | } 320 | return true; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "67.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.4.1" 20 | ansi_regex: 21 | dependency: transitive 22 | description: 23 | name: ansi_regex 24 | sha256: ca4f2b24a85e797a1512e1d3fe34d5f8429648f78e2268b6a8b5628c8430e643 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "0.1.2" 28 | ansi_strip: 29 | dependency: transitive 30 | description: 31 | name: ansi_strip 32 | sha256: "9bb54e10962ac1de86b9b64a278a5b8965a28a2f741975eac7fe9fb0ebe1aaac" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "0.1.1+1" 36 | archive: 37 | dependency: "direct main" 38 | description: 39 | name: archive 40 | sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "3.4.10" 44 | args: 45 | dependency: "direct main" 46 | description: 47 | name: args 48 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.4.2" 52 | async: 53 | dependency: transitive 54 | description: 55 | name: async 56 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.11.0" 60 | boolean_selector: 61 | dependency: transitive 62 | description: 63 | name: boolean_selector 64 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.1.1" 68 | chalkdart: 69 | dependency: "direct main" 70 | description: 71 | name: chalkdart 72 | sha256: "0b7ec5c6a6bafd1445500632c00c573722bd7736e491675d4ac3fe560bbd9cfe" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "2.2.1" 76 | characters: 77 | dependency: transitive 78 | description: 79 | name: characters 80 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.3.0" 84 | cli_spin: 85 | dependency: "direct main" 86 | description: 87 | name: cli_spin 88 | sha256: d942e605e508820b8bde1cfc26f19810abd6bd6bfa32a4f049c9ebaaccbddb50 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.0.1" 92 | cli_table: 93 | dependency: "direct main" 94 | description: 95 | name: cli_table 96 | sha256: "61b61c6dbfa248d8ec9c65b1d97d1ec1952482765563533087ec550405def016" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "1.0.2" 100 | clock: 101 | dependency: transitive 102 | description: 103 | name: clock 104 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "1.1.1" 108 | collection: 109 | dependency: transitive 110 | description: 111 | name: collection 112 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.18.0" 116 | convert: 117 | dependency: transitive 118 | description: 119 | name: convert 120 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "3.1.1" 124 | coverage: 125 | dependency: transitive 126 | description: 127 | name: coverage 128 | sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.7.2" 132 | crypto: 133 | dependency: transitive 134 | description: 135 | name: crypto 136 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "3.0.3" 140 | east_asian_width: 141 | dependency: transitive 142 | description: 143 | name: east_asian_width 144 | sha256: a13c5487dab7ddbad48875789819f0ea38a61cbaaa3024ebe7b199521e6f5788 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.0.1" 148 | emoji_regex: 149 | dependency: transitive 150 | description: 151 | name: emoji_regex 152 | sha256: "3a25dd4d16f98b6f76dc37cc9ae49b8511891ac4b87beac9443a1e9f4634b6c7" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "0.0.5" 156 | file: 157 | dependency: transitive 158 | description: 159 | name: file 160 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "7.0.0" 164 | frontend_server_client: 165 | dependency: transitive 166 | description: 167 | name: frontend_server_client 168 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "3.2.0" 172 | glob: 173 | dependency: transitive 174 | description: 175 | name: glob 176 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "2.1.2" 180 | http: 181 | dependency: "direct main" 182 | description: 183 | name: http 184 | sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.2.1" 188 | http_multi_server: 189 | dependency: transitive 190 | description: 191 | name: http_multi_server 192 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "3.2.1" 196 | http_parser: 197 | dependency: transitive 198 | description: 199 | name: http_parser 200 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "4.0.2" 204 | intl: 205 | dependency: "direct main" 206 | description: 207 | name: intl 208 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "0.19.0" 212 | io: 213 | dependency: transitive 214 | description: 215 | name: io 216 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.0.4" 220 | js: 221 | dependency: transitive 222 | description: 223 | name: js 224 | sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "0.7.1" 228 | lints: 229 | dependency: "direct dev" 230 | description: 231 | name: lints 232 | sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "2.1.1" 236 | logging: 237 | dependency: transitive 238 | description: 239 | name: logging 240 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "1.2.0" 244 | matcher: 245 | dependency: transitive 246 | description: 247 | name: matcher 248 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "0.12.16+1" 252 | meta: 253 | dependency: transitive 254 | description: 255 | name: meta 256 | sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "1.12.0" 260 | mime: 261 | dependency: transitive 262 | description: 263 | name: mime 264 | sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "1.0.5" 268 | node_preamble: 269 | dependency: transitive 270 | description: 271 | name: node_preamble 272 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "2.0.2" 276 | package_config: 277 | dependency: transitive 278 | description: 279 | name: package_config 280 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "2.1.0" 284 | path: 285 | dependency: "direct main" 286 | description: 287 | name: path 288 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "1.9.0" 292 | pointycastle: 293 | dependency: transitive 294 | description: 295 | name: pointycastle 296 | sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "3.7.4" 300 | pool: 301 | dependency: transitive 302 | description: 303 | name: pool 304 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "1.5.1" 308 | pub_semver: 309 | dependency: transitive 310 | description: 311 | name: pub_semver 312 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "2.1.4" 316 | shelf: 317 | dependency: transitive 318 | description: 319 | name: shelf 320 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "1.4.1" 324 | shelf_packages_handler: 325 | dependency: transitive 326 | description: 327 | name: shelf_packages_handler 328 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "3.0.2" 332 | shelf_static: 333 | dependency: transitive 334 | description: 335 | name: shelf_static 336 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "1.1.2" 340 | shelf_web_socket: 341 | dependency: transitive 342 | description: 343 | name: shelf_web_socket 344 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.0.4" 348 | source_map_stack_trace: 349 | dependency: transitive 350 | description: 351 | name: source_map_stack_trace 352 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "2.1.1" 356 | source_maps: 357 | dependency: transitive 358 | description: 359 | name: source_maps 360 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "0.10.12" 364 | source_span: 365 | dependency: transitive 366 | description: 367 | name: source_span 368 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "1.10.0" 372 | stack_trace: 373 | dependency: transitive 374 | description: 375 | name: stack_trace 376 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "1.11.1" 380 | stream_channel: 381 | dependency: transitive 382 | description: 383 | name: stream_channel 384 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "2.1.2" 388 | string_scanner: 389 | dependency: transitive 390 | description: 391 | name: string_scanner 392 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 393 | url: "https://pub.dev" 394 | source: hosted 395 | version: "1.2.0" 396 | string_width: 397 | dependency: transitive 398 | description: 399 | name: string_width 400 | sha256: "0ea481fbb6d5e2d70937fea303d8cc9296048da107dffeecf2acb675c8b47e7f" 401 | url: "https://pub.dev" 402 | source: hosted 403 | version: "0.1.5" 404 | term_glyph: 405 | dependency: transitive 406 | description: 407 | name: term_glyph 408 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 409 | url: "https://pub.dev" 410 | source: hosted 411 | version: "1.2.1" 412 | test: 413 | dependency: "direct dev" 414 | description: 415 | name: test 416 | sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" 417 | url: "https://pub.dev" 418 | source: hosted 419 | version: "1.25.2" 420 | test_api: 421 | dependency: transitive 422 | description: 423 | name: test_api 424 | sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" 425 | url: "https://pub.dev" 426 | source: hosted 427 | version: "0.7.0" 428 | test_core: 429 | dependency: transitive 430 | description: 431 | name: test_core 432 | sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" 433 | url: "https://pub.dev" 434 | source: hosted 435 | version: "0.6.0" 436 | typed_data: 437 | dependency: transitive 438 | description: 439 | name: typed_data 440 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 441 | url: "https://pub.dev" 442 | source: hosted 443 | version: "1.3.2" 444 | vm_service: 445 | dependency: transitive 446 | description: 447 | name: vm_service 448 | sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 449 | url: "https://pub.dev" 450 | source: hosted 451 | version: "14.0.0" 452 | watcher: 453 | dependency: transitive 454 | description: 455 | name: watcher 456 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 457 | url: "https://pub.dev" 458 | source: hosted 459 | version: "1.1.0" 460 | wcwidth: 461 | dependency: transitive 462 | description: 463 | name: wcwidth 464 | sha256: "4e68ce25701e56647cb305ab6d8c75fce5e5196227bcb6ba6886513ac36474c2" 465 | url: "https://pub.dev" 466 | source: hosted 467 | version: "0.0.4" 468 | web: 469 | dependency: transitive 470 | description: 471 | name: web 472 | sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" 473 | url: "https://pub.dev" 474 | source: hosted 475 | version: "0.5.0" 476 | web_socket_channel: 477 | dependency: transitive 478 | description: 479 | name: web_socket_channel 480 | sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" 481 | url: "https://pub.dev" 482 | source: hosted 483 | version: "2.4.4" 484 | webkit_inspection_protocol: 485 | dependency: transitive 486 | description: 487 | name: webkit_inspection_protocol 488 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 489 | url: "https://pub.dev" 490 | source: hosted 491 | version: "1.2.1" 492 | yaml: 493 | dependency: "direct main" 494 | description: 495 | name: yaml 496 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 497 | url: "https://pub.dev" 498 | source: hosted 499 | version: "3.1.2" 500 | sdks: 501 | dart: ">=3.3.0 <4.0.0" 502 | -------------------------------------------------------------------------------- /lib/core/service/package_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chalkdart/chalkstrings.dart'; 4 | import 'package:gpm/core/logging/exit_codes.dart'; 5 | import 'package:yaml/yaml.dart'; 6 | 7 | import '../../extras/extras.dart'; 8 | import '../../gpm_cli.dart'; 9 | import '../provider/compatible_asset_provider.dart'; 10 | import 'api_service.dart'; 11 | import 'build_service.dart'; 12 | import 'installation_service.dart'; 13 | import 'package_disintegration_service.dart'; 14 | import 'package_registry_service.dart'; 15 | import 'system_service.dart'; 16 | import 'update_service.dart'; 17 | import 'upgrade_service.dart'; 18 | 19 | class PackageService { 20 | PackageService._(); 21 | 22 | static Future<String?> findFullQualifiedID(String? id, 23 | {bool searchOnGitHub = true}) async { 24 | if (id == null) { 25 | return null; 26 | } 27 | if (id.contains('/')) { 28 | return id; 29 | } 30 | final releases = PackageRegistryService.getInstalledReleases(); 31 | for (final package in releases) { 32 | if (package.repo == id) { 33 | return package.appID; 34 | } 35 | } 36 | final sources = PackageRegistryService.getInstalledSources(); 37 | for (final package in sources) { 38 | if (package.repo == id) { 39 | return package.appID; 40 | } 41 | } 42 | // searching on github 43 | if (searchOnGitHub) { 44 | print("Searching on github ..."); 45 | final searchResult = await APIService.searchOnGitHub(id); 46 | final githubPackages = 47 | searchResult.isEmpty ? <String>[] : searchResult.keys.toList(); 48 | if (githubPackages.isEmpty) { 49 | print("Could'nt find a package with that name."); 50 | } else { 51 | if (githubPackages.length == 1) { 52 | print( 53 | "Found a package: ☆ ${searchResult[githubPackages.first]} - ${githubPackages.first.bold}"); 54 | if (yes("Are you looking for this one? (y/N): ")) { 55 | return githubPackages.first; 56 | } else { 57 | print("Couldn't find any packages with the name: $id"); 58 | } 59 | } else { 60 | print("Found ${githubPackages.length} packages with this name ..."); 61 | for (final package in githubPackages) { 62 | int index = githubPackages.indexOf(package) + 1; 63 | print( 64 | "${index > 9 ? "" : " "}$index) ☆ ${searchResult[package]} \t- $package"); 65 | } 66 | stdout.write("Please select one of the above packages: ".bold); 67 | int index = int.tryParse(stdin.readLineSync() ?? "0") ?? 0; 68 | if (--index >= 0 && index < githubPackages.length) { 69 | print("Selected: ${githubPackages[index].bold}"); 70 | return githubPackages[index]; 71 | } else { 72 | print("Invalid index selected: ${++index}"); 73 | } 74 | } 75 | } 76 | } else { 77 | print("Could'nt find a package installed with that name."); 78 | } 79 | 80 | terminateInstance(exitCode: ExitCodes.unavailable); 81 | return null; 82 | } 83 | 84 | static void handleInstall(String? id, {required bool explicitCall}) async { 85 | id = await findFullQualifiedID(id); 86 | if (id != null) { 87 | final exists = await APIService.doesRepoExists(id); 88 | final repo = parseRepoName(id); 89 | if (exists) { 90 | await _handlePackageInstall(id, repo, explicitCall); 91 | } else { 92 | print("Repository \"$repo\" does not exists or is a private repo."); 93 | 94 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 95 | } 96 | } 97 | } 98 | 99 | static Future<void> _handlePackageInstall( 100 | String id, String repo, bool explicitCall) async { 101 | // Checking if the package is already installed 102 | if (PackageRegistryService.isPackageInstalled(id)) { 103 | await handleUpdate(id, explicitCall: explicitCall); 104 | return; 105 | } 106 | 107 | if (mode == 'release') { 108 | /// RELEASE MODE 109 | // When package is not installed 110 | // we start fetching the [targetTag] release from github 111 | print("Fetching $targetTag release from $repo ..."); 112 | final assets = await APIService.fetchAssets(id); 113 | if (assets.isEmpty) { 114 | print( 115 | "${"$repo has no asset in its ".red}${"$targetTag release.".bold.red}"); 116 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 117 | } else { 118 | print( 119 | "Identifying ${SystemService.os} compatible release candidates ..."); 120 | final provider = CompatibleReleaseAssetProvider.fromList( 121 | assets: assets, 122 | target: SystemService.osObject, 123 | ); 124 | int total = 0; 125 | String targetType = 'compressed (zip or other)'; 126 | final extensions = ExtensionPriorities.getExtensionsWithPriorityOrder( 127 | SystemService.osObject); 128 | if (provider.hasPrimary) { 129 | total += provider.primary.length; 130 | targetType = extensions.first; 131 | } else if (provider.hasSecondary) { 132 | total += provider.secondary.length; 133 | targetType = extensions[1]; 134 | } else { 135 | total += provider.others.length; 136 | } 137 | if (total > 0) { 138 | print("Found $total $targetType release candidates."); 139 | await InstallationService.initReleaseInstall( 140 | repo, provider, extensions, explicitCall); 141 | 142 | if (explicitCall) terminateInstance(); 143 | } else { 144 | print("No installable candidates found".red); 145 | 146 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 147 | } 148 | } 149 | } else { 150 | await handleSourceModeInstall(id, repo, explicitCall); 151 | } 152 | } 153 | 154 | static Future<bool> handleSourceModeInstall( 155 | id, repo, bool explicitCall) async { 156 | // SOURCE MODE 157 | print('Getting Build Instructions ...'); 158 | final spec = await APIService.getGPMSpecification(id); 159 | if (spec == null) { 160 | print( 161 | '$repo does not have gpm.yaml, cannot install from source.'.red.bold, 162 | ); 163 | print( 164 | 'Please create an issue to support gpm at https://github.com/$id/issues/new' 165 | .bold 166 | .dim); 167 | 168 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 169 | } else { 170 | final result = await BuildService.handleBuildFromSource(id, repo, spec); 171 | if (explicitCall) { 172 | terminateInstance( 173 | exitCode: result ? ExitCodes.fine : ExitCodes.error, 174 | ); 175 | } 176 | return result; 177 | } 178 | return false; 179 | } 180 | 181 | static void handleRemove(String? id) async { 182 | id = await findFullQualifiedID(id, searchOnGitHub: false); 183 | if (id != null) { 184 | if (!PackageRegistryService.isPackageInstalled(id)) { 185 | print("Couldn't find ${parseRepoName(id).red.bold} in package list."); 186 | terminateInstance(exitCode: ExitCodes.unavailable); 187 | } else { 188 | PackageDisintegrationService.disintegrate(id); 189 | } 190 | } 191 | } 192 | 193 | static Future<bool> handleUpdate(String? id, 194 | {required bool explicitCall}) async { 195 | id = await findFullQualifiedID(id); 196 | if (id != null) { 197 | final exists = await APIService.doesRepoExists(id); 198 | final repo = parseRepoName(id); 199 | if (exists) { 200 | return await handlePackageUpdate(id, repo, explicitCall); 201 | } else { 202 | print("Repository \"$repo\" does not exists or is a private repo."); 203 | 204 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 205 | } 206 | } 207 | return false; 208 | } 209 | 210 | static Future<bool> handlePackageUpdate( 211 | String id, 212 | String repo, 213 | bool explicitCall, { 214 | bool showUpdateLogs = true, 215 | bool forceTarget = false, 216 | String? forceTag, 217 | bool forceCommit = false, 218 | String? forceCommitHash, 219 | }) async { 220 | // first, we check if the app is installed in the registry 221 | if (PackageRegistryService.isPackageInstalled(id)) { 222 | // checking the previous mode of installation 223 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 224 | final release = PackageRegistryService.getReleaseObject(id); 225 | if (targetTag == 'latest' && release.explicitVersion) { 226 | // this means that the user is normally trying to update 227 | // a package which was specificially installed with certain tag name 228 | print( 229 | 'WARNING: You explicitly installed $repo with tag ${release.tag},' 230 | .yellow 231 | .dim); 232 | print( 233 | 'To update it, you also need to provide the tag name using --tag option, see help.\n' 234 | .yellow 235 | .dim); 236 | } else if (targetTag != 'latest') { 237 | forceTag = targetTag; 238 | } 239 | if (forceTarget) { 240 | targetTag = forceTag; 241 | } 242 | if (showUpdateLogs) { 243 | print("Checking for ${forceTag ?? "updates"} ..."); 244 | } 245 | // further, we compare the tags 246 | final latestTag = forceTag ?? await APIService.getLatestTag(id); 247 | if (latestTag == release.tag) { 248 | print( 249 | '$repo is already installed with the latest version ${release.tag}'); 250 | } else { 251 | if (showUpdateLogs) { 252 | print('${release.tag} is installed'); 253 | } 254 | if (latestTag == null) { 255 | print('Unable to check for updates for $repo!'.red.bold); 256 | } else { 257 | if (showUpdateLogs) { 258 | print('However, ${latestTag.blue.bold} is available ...'); 259 | } 260 | if (yes('Proceed to update (y/N): ')) { 261 | final assets = await APIService.fetchAssets(id); 262 | if (assets.isEmpty) { 263 | print( 264 | "${"$repo has no asset in its ".red}${"latest release.".bold.red}"); 265 | } else { 266 | print( 267 | "Identifying ${SystemService.os} compatible release candidates ..."); 268 | final provider = CompatibleReleaseAssetProvider.fromList( 269 | assets: assets, 270 | target: SystemService.osObject, 271 | ); 272 | 273 | if (_hasCompatibleReleaseTarget(release.type, provider)) { 274 | final extensions = 275 | ExtensionPriorities.getExtensionsWithPriorityOrder( 276 | SystemService.osObject); 277 | final isChosenIndexAvailable = 278 | await UpdateService.initReleaseUpdate( 279 | repo, 280 | provider, 281 | extensions, 282 | release, 283 | explicitCall, 284 | ); 285 | if (!isChosenIndexAvailable) { 286 | print( 287 | 'The source repo no longer provides the choice of release you made: #${release.index}' 288 | .red 289 | .bold); 290 | print('You need to remove and reinstall $id.'.red.bold); 291 | 292 | if (explicitCall) { 293 | terminateInstance(exitCode: ExitCodes.unavailable); 294 | } 295 | } else { 296 | if (explicitCall) terminateInstance(); 297 | return true; 298 | } 299 | } else { 300 | print( 301 | 'The source repo no longer provides the type of release you installed' 302 | .red 303 | .bold); 304 | print('You need to remove and reinstall $id.'.red.bold); 305 | } 306 | } 307 | } 308 | } 309 | } 310 | } else { 311 | print('Checking for updates ...'); 312 | final sourceEntity = PackageRegistryService.getSourceObject(id); 313 | if (forceCommit) { 314 | commitHash = forceCommitHash; 315 | } 316 | final latestCommit = forceCommit 317 | ? forceCommitHash 318 | : (commitHash ?? await APIService.getLatestCommit(id)); 319 | if (latestCommit != null) { 320 | // the stored commit hash in registry is actually the full code 321 | // and the one we are getting through the API is the short code 322 | // so, we need to cut-out the short code from the full code 323 | final currentCommit = sourceEntity.commitHash.substring(0, 7); 324 | if (currentCommit == latestCommit) { 325 | print( 326 | '$repo is already installed with the latest commit $currentCommit'); 327 | } else { 328 | print("Currently Installed Commit \"$currentCommit\" ..."); 329 | print("However, Commit \"$latestCommit\" is available ..."); 330 | if (yes( 331 | 'Do you want to build from source with the ${forceCommit ? forceCommitHash : "latest"} commit? (y/N): ')) { 332 | return await handleSourceModeInstall(id, repo, explicitCall); 333 | } 334 | } 335 | } else { 336 | print('Couldn\'t get latest commit from GitHub.'); 337 | } 338 | } 339 | } 340 | // the app wasn't installed so, it wasn't updated by `gpm --update` 341 | 342 | if (explicitCall) terminateInstance(); 343 | return false; 344 | } 345 | 346 | static bool _hasCompatibleReleaseTarget( 347 | String type, CompatibleReleaseAssetProvider provider) { 348 | if (type == 'primary') { 349 | for (var asset in provider.primary) { 350 | asset.type = type; 351 | } 352 | return provider.hasPrimary; 353 | } else if (type == 'secondary') { 354 | for (var asset in provider.secondary) { 355 | asset.type = type; 356 | } 357 | return provider.hasSecondary; 358 | } else if (type == 'others') { 359 | for (var asset in provider.others) { 360 | asset.type = type; 361 | } 362 | return provider.hasOthers; 363 | } 364 | return false; 365 | } 366 | 367 | static void handleUpgrade(_) async { 368 | if (_) { 369 | // fetching a list of installed apps 370 | final appIDs = PackageRegistryService.getInstalledApps(); 371 | if (appIDs.isEmpty) { 372 | print('Couldn\'t find any packages installed via gpm.'); 373 | } else { 374 | UpgradeService.doUpgrade(appIDs); 375 | } 376 | } 377 | } 378 | 379 | static void handleRollback(String? id, {required bool explicitCall}) async { 380 | id = await findFullQualifiedID(id, searchOnGitHub: false); 381 | if (id != null) { 382 | final exists = await APIService.doesRepoExists(id); 383 | final repo = parseRepoName(id); 384 | if (exists) { 385 | await handlePackageRollback(id, repo, explicitCall); 386 | } else { 387 | print("Repository \"$repo\" does not exists or is a private repo."); 388 | 389 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 390 | } 391 | } 392 | } 393 | 394 | static Future<void> handlePackageRollback( 395 | String id, String repo, bool explicitCall) async { 396 | if (PackageRegistryService.isPackageInstalled(id)) { 397 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 398 | print('Getting previous versions ...'); 399 | final release = PackageRegistryService.getReleaseObject(id); 400 | final index = release.versions.indexOf(release.tag) - 1; 401 | final targetable = index >= 0; 402 | if (!targetable) { 403 | print('No previous versions available to rollback $repo.'.red.bold); 404 | 405 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 406 | } else { 407 | await handlePackageUpdate( 408 | id, 409 | repo, 410 | explicitCall, 411 | forceTag: release.versions[index], 412 | forceTarget: true, 413 | ); 414 | } 415 | } else { 416 | print('Getting previous commits ...'); 417 | final source = PackageRegistryService.getSourceObject(id); 418 | final versions = PackageRegistryService.getSourceCommits(id); 419 | final index = versions.indexOf(source.commitHash) - 1; 420 | final targetable = index >= 0; 421 | if (!targetable) { 422 | print('No previous commits available to rollback $repo.'.red.bold); 423 | 424 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 425 | } else { 426 | await handlePackageUpdate( 427 | id, 428 | repo, 429 | explicitCall, 430 | forceCommitHash: versions[index], 431 | forceCommit: true, 432 | ); 433 | } 434 | } 435 | } else { 436 | print('Couldn\'t find $repo in installed packages.'); 437 | 438 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 439 | } 440 | } 441 | 442 | static void handleRollforward(String? id, 443 | {required bool explicitCall}) async { 444 | id = await findFullQualifiedID(id, searchOnGitHub: false); 445 | if (id != null) { 446 | final exists = await APIService.doesRepoExists(id); 447 | final repo = parseRepoName(id); 448 | if (exists) { 449 | await handlePackageRollforward(id, repo, explicitCall); 450 | } else { 451 | print("Repository \"$repo\" does not exists or is a private repo."); 452 | 453 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 454 | } 455 | } 456 | } 457 | 458 | static Future<void> handlePackageRollforward( 459 | String id, String repo, bool explicitCall) async { 460 | if (PackageRegistryService.isPackageInstalled(id)) { 461 | if (PackageRegistryService.isPackageInstalledViaReleaseMode(id)) { 462 | print('Getting newer versions ...'); 463 | final release = PackageRegistryService.getReleaseObject(id); 464 | final index = release.versions.indexOf(release.tag) + 1; 465 | final targetable = index < release.versions.length; 466 | if (!targetable) { 467 | print('No newer versions available to rollforward $repo.'.red.bold); 468 | 469 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 470 | } else { 471 | await handlePackageUpdate( 472 | id, 473 | repo, 474 | explicitCall, 475 | forceTag: release.versions[index], 476 | forceTarget: true, 477 | ); 478 | } 479 | } else { 480 | print('Getting newer commits ...'); 481 | final source = PackageRegistryService.getSourceObject(id); 482 | final versions = PackageRegistryService.getSourceCommits(id); 483 | final index = versions.indexOf(source.commitHash) + 1; 484 | final targetable = index < versions.length; 485 | if (!targetable) { 486 | print('No newer commits available to rollforward $repo.'.red.bold); 487 | 488 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 489 | } else { 490 | await handlePackageUpdate( 491 | id, 492 | repo, 493 | explicitCall, 494 | forceCommitHash: versions[index], 495 | forceCommit: true, 496 | ); 497 | } 498 | } 499 | } else { 500 | print('Couldn\'t find $repo in installed packages.'); 501 | 502 | if (explicitCall) terminateInstance(exitCode: ExitCodes.unavailable); 503 | } 504 | } 505 | 506 | static Future<void> buildLocally(String? id) async { 507 | id = await findFullQualifiedID(id, searchOnGitHub: false); 508 | if (id != null) { 509 | // checking `gpm.yaml` specification 510 | final specFile = File('gpm.yaml'); 511 | if (!specFile.existsSync()) { 512 | print( 513 | 'Couldn\'t find `gpm.yaml` specification file in the current directory.' 514 | .red 515 | .bold); 516 | terminateInstance(exitCode: ExitCodes.unavailable); 517 | return; 518 | } 519 | 520 | // reading specification 521 | final contents = specFile.readAsStringSync(); 522 | final spec = await loadYaml(contents); 523 | final result = await BuildService.handleBuildFromSource( 524 | id, 525 | parseRepoName(id), 526 | spec, 527 | isLocalBuild: true, 528 | ); 529 | terminateInstance( 530 | exitCode: result ? ExitCodes.fine : ExitCodes.error, 531 | ); 532 | } 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_Packages.xml: -------------------------------------------------------------------------------- 1 | <component name="libraryTable"> 2 | <library name="Dart Packages" type="DartPackagesLibraryType"> 3 | <properties> 4 | <option name="packageNameToDirsMap"> 5 | <entry key="_fe_analyzer_shared"> 6 | <value> 7 | <list> 8 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-67.0.0/lib" /> 9 | </list> 10 | </value> 11 | </entry> 12 | <entry key="analyzer"> 13 | <value> 14 | <list> 15 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/analyzer-6.4.1/lib" /> 16 | </list> 17 | </value> 18 | </entry> 19 | <entry key="ansi_regex"> 20 | <value> 21 | <list> 22 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/ansi_regex-0.1.2/lib" /> 23 | </list> 24 | </value> 25 | </entry> 26 | <entry key="ansi_strip"> 27 | <value> 28 | <list> 29 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/ansi_strip-0.1.1+1/lib" /> 30 | </list> 31 | </value> 32 | </entry> 33 | <entry key="archive"> 34 | <value> 35 | <list> 36 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/archive-3.4.10/lib" /> 37 | </list> 38 | </value> 39 | </entry> 40 | <entry key="args"> 41 | <value> 42 | <list> 43 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/args-2.4.2/lib" /> 44 | </list> 45 | </value> 46 | </entry> 47 | <entry key="async"> 48 | <value> 49 | <list> 50 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/async-2.11.0/lib" /> 51 | </list> 52 | </value> 53 | </entry> 54 | <entry key="boolean_selector"> 55 | <value> 56 | <list> 57 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/lib" /> 58 | </list> 59 | </value> 60 | </entry> 61 | <entry key="chalkdart"> 62 | <value> 63 | <list> 64 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/chalkdart-2.2.1/lib" /> 65 | </list> 66 | </value> 67 | </entry> 68 | <entry key="characters"> 69 | <value> 70 | <list> 71 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/characters-1.3.0/lib" /> 72 | </list> 73 | </value> 74 | </entry> 75 | <entry key="cli_spin"> 76 | <value> 77 | <list> 78 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/cli_spin-1.0.1/lib" /> 79 | </list> 80 | </value> 81 | </entry> 82 | <entry key="cli_table"> 83 | <value> 84 | <list> 85 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/cli_table-1.0.2/lib" /> 86 | </list> 87 | </value> 88 | </entry> 89 | <entry key="clock"> 90 | <value> 91 | <list> 92 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/clock-1.1.1/lib" /> 93 | </list> 94 | </value> 95 | </entry> 96 | <entry key="collection"> 97 | <value> 98 | <list> 99 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.18.0/lib" /> 100 | </list> 101 | </value> 102 | </entry> 103 | <entry key="convert"> 104 | <value> 105 | <list> 106 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/convert-3.1.1/lib" /> 107 | </list> 108 | </value> 109 | </entry> 110 | <entry key="coverage"> 111 | <value> 112 | <list> 113 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/coverage-1.7.2/lib" /> 114 | </list> 115 | </value> 116 | </entry> 117 | <entry key="crypto"> 118 | <value> 119 | <list> 120 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/crypto-3.0.3/lib" /> 121 | </list> 122 | </value> 123 | </entry> 124 | <entry key="east_asian_width"> 125 | <value> 126 | <list> 127 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/east_asian_width-1.0.1/lib" /> 128 | </list> 129 | </value> 130 | </entry> 131 | <entry key="emoji_regex"> 132 | <value> 133 | <list> 134 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/emoji_regex-0.0.5/lib" /> 135 | </list> 136 | </value> 137 | </entry> 138 | <entry key="file"> 139 | <value> 140 | <list> 141 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/file-7.0.0/lib" /> 142 | </list> 143 | </value> 144 | </entry> 145 | <entry key="frontend_server_client"> 146 | <value> 147 | <list> 148 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/frontend_server_client-3.2.0/lib" /> 149 | </list> 150 | </value> 151 | </entry> 152 | <entry key="glob"> 153 | <value> 154 | <list> 155 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/glob-2.1.2/lib" /> 156 | </list> 157 | </value> 158 | </entry> 159 | <entry key="http"> 160 | <value> 161 | <list> 162 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/http-1.2.1/lib" /> 163 | </list> 164 | </value> 165 | </entry> 166 | <entry key="http_multi_server"> 167 | <value> 168 | <list> 169 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/http_multi_server-3.2.1/lib" /> 170 | </list> 171 | </value> 172 | </entry> 173 | <entry key="http_parser"> 174 | <value> 175 | <list> 176 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/http_parser-4.0.2/lib" /> 177 | </list> 178 | </value> 179 | </entry> 180 | <entry key="intl"> 181 | <value> 182 | <list> 183 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/intl-0.19.0/lib" /> 184 | </list> 185 | </value> 186 | </entry> 187 | <entry key="io"> 188 | <value> 189 | <list> 190 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/io-1.0.4/lib" /> 191 | </list> 192 | </value> 193 | </entry> 194 | <entry key="js"> 195 | <value> 196 | <list> 197 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/js-0.7.1/lib" /> 198 | </list> 199 | </value> 200 | </entry> 201 | <entry key="lints"> 202 | <value> 203 | <list> 204 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/lints-2.1.1/lib" /> 205 | </list> 206 | </value> 207 | </entry> 208 | <entry key="logging"> 209 | <value> 210 | <list> 211 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/logging-1.2.0/lib" /> 212 | </list> 213 | </value> 214 | </entry> 215 | <entry key="matcher"> 216 | <value> 217 | <list> 218 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/matcher-0.12.16+1/lib" /> 219 | </list> 220 | </value> 221 | </entry> 222 | <entry key="meta"> 223 | <value> 224 | <list> 225 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/meta-1.12.0/lib" /> 226 | </list> 227 | </value> 228 | </entry> 229 | <entry key="mime"> 230 | <value> 231 | <list> 232 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/mime-1.0.5/lib" /> 233 | </list> 234 | </value> 235 | </entry> 236 | <entry key="node_preamble"> 237 | <value> 238 | <list> 239 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/node_preamble-2.0.2/lib" /> 240 | </list> 241 | </value> 242 | </entry> 243 | <entry key="package_config"> 244 | <value> 245 | <list> 246 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/package_config-2.1.0/lib" /> 247 | </list> 248 | </value> 249 | </entry> 250 | <entry key="path"> 251 | <value> 252 | <list> 253 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/path-1.9.0/lib" /> 254 | </list> 255 | </value> 256 | </entry> 257 | <entry key="pointycastle"> 258 | <value> 259 | <list> 260 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/pointycastle-3.7.4/lib" /> 261 | </list> 262 | </value> 263 | </entry> 264 | <entry key="pool"> 265 | <value> 266 | <list> 267 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/pool-1.5.1/lib" /> 268 | </list> 269 | </value> 270 | </entry> 271 | <entry key="pub_semver"> 272 | <value> 273 | <list> 274 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/lib" /> 275 | </list> 276 | </value> 277 | </entry> 278 | <entry key="shelf"> 279 | <value> 280 | <list> 281 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf-1.4.1/lib" /> 282 | </list> 283 | </value> 284 | </entry> 285 | <entry key="shelf_packages_handler"> 286 | <value> 287 | <list> 288 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2/lib" /> 289 | </list> 290 | </value> 291 | </entry> 292 | <entry key="shelf_static"> 293 | <value> 294 | <list> 295 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_static-1.1.2/lib" /> 296 | </list> 297 | </value> 298 | </entry> 299 | <entry key="shelf_web_socket"> 300 | <value> 301 | <list> 302 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_web_socket-1.0.4/lib" /> 303 | </list> 304 | </value> 305 | </entry> 306 | <entry key="source_map_stack_trace"> 307 | <value> 308 | <list> 309 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.1/lib" /> 310 | </list> 311 | </value> 312 | </entry> 313 | <entry key="source_maps"> 314 | <value> 315 | <list> 316 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/source_maps-0.10.12/lib" /> 317 | </list> 318 | </value> 319 | </entry> 320 | <entry key="source_span"> 321 | <value> 322 | <list> 323 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/source_span-1.10.0/lib" /> 324 | </list> 325 | </value> 326 | </entry> 327 | <entry key="stack_trace"> 328 | <value> 329 | <list> 330 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/lib" /> 331 | </list> 332 | </value> 333 | </entry> 334 | <entry key="stream_channel"> 335 | <value> 336 | <list> 337 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/stream_channel-2.1.2/lib" /> 338 | </list> 339 | </value> 340 | </entry> 341 | <entry key="string_scanner"> 342 | <value> 343 | <list> 344 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/string_scanner-1.2.0/lib" /> 345 | </list> 346 | </value> 347 | </entry> 348 | <entry key="string_width"> 349 | <value> 350 | <list> 351 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/string_width-0.1.5/lib" /> 352 | </list> 353 | </value> 354 | </entry> 355 | <entry key="term_glyph"> 356 | <value> 357 | <list> 358 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/term_glyph-1.2.1/lib" /> 359 | </list> 360 | </value> 361 | </entry> 362 | <entry key="test"> 363 | <value> 364 | <list> 365 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/test-1.25.2/lib" /> 366 | </list> 367 | </value> 368 | </entry> 369 | <entry key="test_api"> 370 | <value> 371 | <list> 372 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/test_api-0.7.0/lib" /> 373 | </list> 374 | </value> 375 | </entry> 376 | <entry key="test_core"> 377 | <value> 378 | <list> 379 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/test_core-0.6.0/lib" /> 380 | </list> 381 | </value> 382 | </entry> 383 | <entry key="typed_data"> 384 | <value> 385 | <list> 386 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/typed_data-1.3.2/lib" /> 387 | </list> 388 | </value> 389 | </entry> 390 | <entry key="vm_service"> 391 | <value> 392 | <list> 393 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/vm_service-14.0.0/lib" /> 394 | </list> 395 | </value> 396 | </entry> 397 | <entry key="watcher"> 398 | <value> 399 | <list> 400 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/watcher-1.1.0/lib" /> 401 | </list> 402 | </value> 403 | </entry> 404 | <entry key="wcwidth"> 405 | <value> 406 | <list> 407 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/wcwidth-0.0.4/lib" /> 408 | </list> 409 | </value> 410 | </entry> 411 | <entry key="web"> 412 | <value> 413 | <list> 414 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/web-0.5.0/lib" /> 415 | </list> 416 | </value> 417 | </entry> 418 | <entry key="web_socket_channel"> 419 | <value> 420 | <list> 421 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/web_socket_channel-2.4.4/lib" /> 422 | </list> 423 | </value> 424 | </entry> 425 | <entry key="webkit_inspection_protocol"> 426 | <value> 427 | <list> 428 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.2.1/lib" /> 429 | </list> 430 | </value> 431 | </entry> 432 | <entry key="yaml"> 433 | <value> 434 | <list> 435 | <option value="$USER_HOME$/.pub-cache/hosted/pub.dev/yaml-3.1.2/lib" /> 436 | </list> 437 | </value> 438 | </entry> 439 | </option> 440 | </properties> 441 | <CLASSES> 442 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-67.0.0/lib" /> 443 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/analyzer-6.4.1/lib" /> 444 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/ansi_regex-0.1.2/lib" /> 445 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/ansi_strip-0.1.1+1/lib" /> 446 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/archive-3.4.10/lib" /> 447 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/args-2.4.2/lib" /> 448 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/async-2.11.0/lib" /> 449 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/lib" /> 450 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/chalkdart-2.2.1/lib" /> 451 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/characters-1.3.0/lib" /> 452 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/cli_spin-1.0.1/lib" /> 453 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/cli_table-1.0.2/lib" /> 454 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/clock-1.1.1/lib" /> 455 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.18.0/lib" /> 456 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/convert-3.1.1/lib" /> 457 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/coverage-1.7.2/lib" /> 458 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/crypto-3.0.3/lib" /> 459 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/east_asian_width-1.0.1/lib" /> 460 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/emoji_regex-0.0.5/lib" /> 461 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/file-7.0.0/lib" /> 462 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/frontend_server_client-3.2.0/lib" /> 463 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/glob-2.1.2/lib" /> 464 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/http-1.2.1/lib" /> 465 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/http_multi_server-3.2.1/lib" /> 466 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/http_parser-4.0.2/lib" /> 467 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/intl-0.19.0/lib" /> 468 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/io-1.0.4/lib" /> 469 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/js-0.7.1/lib" /> 470 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/lints-2.1.1/lib" /> 471 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/logging-1.2.0/lib" /> 472 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/matcher-0.12.16+1/lib" /> 473 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/meta-1.12.0/lib" /> 474 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/mime-1.0.5/lib" /> 475 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/node_preamble-2.0.2/lib" /> 476 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/package_config-2.1.0/lib" /> 477 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path-1.9.0/lib" /> 478 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pointycastle-3.7.4/lib" /> 479 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pool-1.5.1/lib" /> 480 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/lib" /> 481 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf-1.4.1/lib" /> 482 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2/lib" /> 483 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_static-1.1.2/lib" /> 484 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_web_socket-1.0.4/lib" /> 485 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.1/lib" /> 486 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/source_maps-0.10.12/lib" /> 487 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/source_span-1.10.0/lib" /> 488 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/lib" /> 489 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/stream_channel-2.1.2/lib" /> 490 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/string_scanner-1.2.0/lib" /> 491 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/string_width-0.1.5/lib" /> 492 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/term_glyph-1.2.1/lib" /> 493 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/test-1.25.2/lib" /> 494 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/test_api-0.7.0/lib" /> 495 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/test_core-0.6.0/lib" /> 496 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/typed_data-1.3.2/lib" /> 497 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/vm_service-14.0.0/lib" /> 498 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/watcher-1.1.0/lib" /> 499 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/wcwidth-0.0.4/lib" /> 500 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/web-0.5.0/lib" /> 501 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/web_socket_channel-2.4.4/lib" /> 502 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.2.1/lib" /> 503 | <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/yaml-3.1.2/lib" /> 504 | </CLASSES> 505 | <JAVADOC /> 506 | <SOURCES /> 507 | </library> 508 | </component> --------------------------------------------------------------------------------