├── LICENSE ├── README.md ├── installer ├── .gitignore ├── windows │ ├── puro.ico │ ├── build_installer.ps1 │ └── install.iss └── install.sh ├── puro ├── lib │ ├── cli.dart │ ├── models.dart │ └── src │ │ ├── engine │ │ ├── patch.dart │ │ ├── depot_tools.dart │ │ ├── dart_version.dart │ │ ├── build_env.dart │ │ └── prepare.dart │ │ ├── proto │ │ ├── puro.pbenum.dart │ │ ├── flutter_releases.pbenum.dart │ │ ├── puro.pbserver.dart │ │ ├── flutter_releases.pbserver.dart │ │ └── flutter_releases.pbjson.dart │ │ ├── workspace │ │ ├── common.dart │ │ ├── clean.dart │ │ ├── gitignore.dart │ │ ├── install.dart │ │ └── vscode.dart │ │ ├── commands │ │ ├── clean.dart │ │ ├── env_rename.dart │ │ ├── gc.dart │ │ ├── build_shell.dart │ │ ├── env_rm.dart │ │ ├── dart.dart │ │ ├── pub.dart │ │ ├── run.dart │ │ ├── env_ls.dart │ │ ├── prefs.dart │ │ ├── version.dart │ │ ├── flutter.dart │ │ ├── env_create.dart │ │ ├── env_upgrade.dart │ │ ├── puro_uninstall.dart │ │ ├── env_use.dart │ │ ├── engine.dart │ │ ├── repl.dart │ │ ├── puro_upgrade.dart │ │ ├── eval.dart │ │ ├── ls_versions.dart │ │ └── puro_install.dart │ │ ├── downloader.dart │ │ ├── string_utils.dart │ │ ├── install │ │ └── upgrade.dart │ │ ├── debouncer.dart │ │ ├── env │ │ ├── delete.dart │ │ ├── rename.dart │ │ ├── default.dart │ │ ├── gc.dart │ │ ├── upgrade.dart │ │ └── releases.dart │ │ ├── ast │ │ └── reader.dart │ │ ├── provider.dart │ │ ├── eval │ │ ├── packages.dart │ │ └── parse.dart │ │ ├── logger.dart │ │ ├── json_edit │ │ ├── grammar.dart │ │ └── element.dart │ │ ├── command_result.dart │ │ └── progress.dart ├── protoc.sh ├── protoc.ps1 ├── bin │ └── puro.dart ├── install_dev.sh ├── install_dev.ps1 ├── build.sh ├── build.ps1 ├── .gitignore ├── proto │ ├── flutter_releases.proto │ └── puro.proto ├── test │ └── json_grammar_test.dart ├── pubspec.yaml ├── LICENSE ├── README.md ├── analysis_options.yaml └── CHANGELOG.md ├── website ├── serve.ps1 ├── .gitignore ├── docs │ ├── assets │ │ ├── puro.png │ │ ├── puro_icon.png │ │ ├── puro_icon_small.png │ │ ├── storage_with_puro.png │ │ ├── storage_without_puro.png │ │ ├── star.svg │ │ ├── giscus_theme_dark.css │ │ └── giscus_theme_light.css │ ├── javascript │ │ └── os_detect.js │ ├── stylesheets │ │ └── extra.css │ └── index.md ├── __pycache__ │ └── pm_attr_list.cpython-310.pyc ├── overrides │ └── partials │ │ ├── tabs-item.html │ │ └── comments.html └── mkdocs.yml ├── .gitignore └── benchmarks ├── Dockerfile ├── git.sh ├── puro.sh ├── puro_switching.sh ├── fvm.sh └── puro_cached.sh /LICENSE: -------------------------------------------------------------------------------- 1 | puro/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | puro/README.md -------------------------------------------------------------------------------- /installer/.gitignore: -------------------------------------------------------------------------------- 1 | # Inno 2 | 3 | windows/Output/ -------------------------------------------------------------------------------- /puro/lib/cli.dart: -------------------------------------------------------------------------------- 1 | export 'src/cli.dart' show main; 2 | -------------------------------------------------------------------------------- /puro/protoc.sh: -------------------------------------------------------------------------------- 1 | protoc -I=proto --dart_out=lib/src/proto proto/*.proto 2 | dart format . -------------------------------------------------------------------------------- /website/serve.ps1: -------------------------------------------------------------------------------- 1 | $env:PYTHONPATH="C:\Users\ping\IdeaProjects\puro\website"; mkdocs serve -------------------------------------------------------------------------------- /puro/protoc.ps1: -------------------------------------------------------------------------------- 1 | protoc -I=proto --dart_out=lib\src\proto proto\*.proto 2 | puro dart format . -------------------------------------------------------------------------------- /puro/lib/models.dart: -------------------------------------------------------------------------------- 1 | export 'src/proto/puro.pb.dart'; 2 | export 'src/proto/puro.pbenum.dart'; 3 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Auto-generated 2 | docs/reference/commands.md 3 | docs/reference/changelog.md -------------------------------------------------------------------------------- /installer/windows/puro.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingbird/puro/HEAD/installer/windows/puro.ico -------------------------------------------------------------------------------- /website/docs/assets/puro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingbird/puro/HEAD/website/docs/assets/puro.png -------------------------------------------------------------------------------- /website/docs/assets/puro_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingbird/puro/HEAD/website/docs/assets/puro_icon.png -------------------------------------------------------------------------------- /website/docs/assets/puro_icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingbird/puro/HEAD/website/docs/assets/puro_icon_small.png -------------------------------------------------------------------------------- /puro/bin/puro.dart: -------------------------------------------------------------------------------- 1 | import 'package:puro/cli.dart' as cli; 2 | 3 | void main(List args) { 4 | cli.main(args); 5 | } 6 | -------------------------------------------------------------------------------- /website/docs/assets/storage_with_puro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingbird/puro/HEAD/website/docs/assets/storage_with_puro.png -------------------------------------------------------------------------------- /website/docs/assets/storage_without_puro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingbird/puro/HEAD/website/docs/assets/storage_without_puro.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ related 2 | 3 | .idea/ 4 | *.iml 5 | 6 | # VSCode 7 | 8 | .vscode/ 9 | 10 | # Obsidian 11 | 12 | .obsidian/ 13 | -------------------------------------------------------------------------------- /puro/install_dev.sh: -------------------------------------------------------------------------------- 1 | # Builds and installs puro from source, for development purposes. 2 | 3 | ./build.sh 4 | bin/puro install-puro --promote 5 | -------------------------------------------------------------------------------- /website/__pycache__/pm_attr_list.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingbird/puro/HEAD/website/__pycache__/pm_attr_list.cpython-310.pyc -------------------------------------------------------------------------------- /puro/install_dev.ps1: -------------------------------------------------------------------------------- 1 | # Builds and installs puro from source, for development purposes. 2 | 3 | & "$PSScriptRoot/build.ps1" 4 | bin/puro.exe install-puro --log-level=4 --promote 5 | -------------------------------------------------------------------------------- /puro/build.sh: -------------------------------------------------------------------------------- 1 | puro_version="$(dart bin/puro.dart version --no-update-check --plain "$@")" 2 | echo "version: $puro_version" 3 | dart compile exe bin/puro.dart -o bin/puro "--define=puro_version=$puro_version" 4 | -------------------------------------------------------------------------------- /puro/build.ps1: -------------------------------------------------------------------------------- 1 | $puro_version = dart bin/puro.dart version --no-update-check --plain @args 2 | Write-Output "Version: $puro_version" 3 | dart compile exe bin/puro.dart -o bin/puro.exe --define=puro_version=$puro_version 4 | -------------------------------------------------------------------------------- /installer/windows/build_installer.ps1: -------------------------------------------------------------------------------- 1 | $puro_version = ..\..\puro\bin\puro.exe version --plain 2 | Write-Output "Version: $puro_version" 3 | &"C:\Program Files (x86)\Inno Setup 6\iscc" "/dAppVersion=${puro_version}" install.iss 4 | if(!$?) { Exit $LASTEXITCODE } -------------------------------------------------------------------------------- /puro/.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build outputs. 6 | build/ 7 | bin/puro 8 | bin/puro.exe 9 | temp_ast_gen 10 | 11 | # Omit committing pubspec.lock for library packages; see 12 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 13 | pubspec.lock -------------------------------------------------------------------------------- /puro/lib/src/engine/patch.dart: -------------------------------------------------------------------------------- 1 | import '../provider.dart'; 2 | 3 | Future applyEnginePatches({ 4 | required Scope scope, 5 | required String engineCommit, 6 | }) async { 7 | // final config = PuroConfig.of(scope); 8 | // final git = GitClient.of(scope); 9 | // final baseCache = config.getFlutterCache(engineCommit, patched: false); 10 | // final patchedCache = config.getFlutterCache(engineCommit, patched: true); 11 | } 12 | -------------------------------------------------------------------------------- /benchmarks/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | RUN apt update -y; apt install -y curl git unzip bc build-essential snapd sudo 3 | RUN /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 4 | RUN echo '# Set PATH, MANPATH, etc., for Homebrew.' >> /root/.profile 5 | RUN echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /root/.profile 6 | RUN eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 7 | ADD ./* $HOME/ -------------------------------------------------------------------------------- /puro/proto/flutter_releases.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message FlutterReleaseModel { 4 | string hash = 1; 5 | string channel = 2; 6 | string version = 3; 7 | string dart_sdk_version = 4; 8 | string dart_sdk_arch = 5; 9 | string release_date = 6; 10 | string archive = 7; 11 | string sha256 = 8; 12 | } 13 | 14 | message FlutterReleasesModel { 15 | string base_url = 1; 16 | map current_release = 2; 17 | repeated FlutterReleaseModel releases = 3; 18 | } -------------------------------------------------------------------------------- /puro/lib/src/proto/puro.pbenum.dart: -------------------------------------------------------------------------------- 1 | // This is a generated file - do not edit. 2 | // 3 | // Generated from puro.proto. 4 | 5 | // @dart = 3.3 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: curly_braces_in_flow_control_structures 10 | // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes 11 | // ignore_for_file: non_constant_identifier_names, prefer_relative_imports 12 | -------------------------------------------------------------------------------- /puro/lib/src/proto/flutter_releases.pbenum.dart: -------------------------------------------------------------------------------- 1 | // This is a generated file - do not edit. 2 | // 3 | // Generated from flutter_releases.proto. 4 | 5 | // @dart = 3.3 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: curly_braces_in_flow_control_structures 10 | // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes 11 | // ignore_for_file: non_constant_identifier_names, prefer_relative_imports 12 | -------------------------------------------------------------------------------- /puro/lib/src/proto/puro.pbserver.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: puro.proto 4 | // 5 | // @dart = 3.3 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes 10 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 11 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 12 | 13 | export 'puro.pb.dart'; 14 | -------------------------------------------------------------------------------- /website/docs/assets/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /puro/lib/src/proto/flutter_releases.pbserver.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: flutter_releases.proto 4 | // 5 | // @dart = 3.3 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes 10 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 11 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 12 | 13 | export 'flutter_releases.pb.dart'; 14 | -------------------------------------------------------------------------------- /puro/lib/src/workspace/common.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | 3 | import '../config.dart'; 4 | import '../provider.dart'; 5 | 6 | abstract class IdeConfig { 7 | IdeConfig({ 8 | required this.workspaceDir, 9 | required this.projectConfig, 10 | this.flutterSdkDir, 11 | this.dartSdkDir, 12 | required this.exists, 13 | }); 14 | String get name; 15 | final ProjectConfig projectConfig; 16 | final Directory workspaceDir; 17 | Directory? flutterSdkDir; 18 | Directory? dartSdkDir; 19 | bool exists; 20 | Future save({required Scope scope}); 21 | Future backup({required Scope scope}); 22 | Future restore({required Scope scope}); 23 | } 24 | -------------------------------------------------------------------------------- /benchmarks/git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | start_time=$(date +%s.%3N) 5 | 6 | cat /proc/net/dev 7 | 8 | git clone https://github.com/flutter/flutter.git 9 | 10 | clone_time=$(date +%s.%3N) 11 | 12 | flutter/bin/flutter --version 13 | 14 | end_time=$(date +%s.%3N) 15 | 16 | clone_duration=$(echo "scale=3; $clone_time - $start_time" | bc) 17 | run_duration=$(echo "scale=3; $end_time - $clone_time" | bc) 18 | total_duration=$(echo "scale=3; $end_time - $start_time" | bc) 19 | echo "Clone: ${clone_duration}s" 20 | echo "Run: ${run_duration}s" 21 | echo "Total: ${total_duration}s" 22 | 23 | total_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 24 | echo "Network: ${total_network} bytes" -------------------------------------------------------------------------------- /puro/lib/src/commands/clean.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../config.dart'; 4 | import '../workspace/clean.dart'; 5 | 6 | class CleanCommand extends PuroCommand { 7 | @override 8 | final name = 'clean'; 9 | 10 | @override 11 | final description = 12 | 'Deletes Puro configuration files from the current project and restores IDE settings'; 13 | 14 | @override 15 | bool get takesArguments => false; 16 | 17 | @override 18 | Future run() async { 19 | final config = PuroConfig.of(scope); 20 | await cleanWorkspace(scope: scope, projectConfig: config.project); 21 | return BasicMessageResult('Removed puro from current project'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /puro/lib/src/commands/env_rename.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../env/rename.dart'; 4 | 5 | class EnvRenameCommand extends PuroCommand { 6 | @override 7 | final name = 'rename'; 8 | 9 | @override 10 | final description = 'Renames an environment'; 11 | 12 | @override 13 | String? get argumentUsage => ' '; 14 | 15 | @override 16 | Future run() async { 17 | final args = unwrapArguments(exactly: 2); 18 | final name = args[0]; 19 | final newName = args[1]; 20 | await renameEnvironment(scope: scope, name: name, newName: newName); 21 | return BasicMessageResult('Renamed environment `$name` to `$newName`'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /puro/lib/src/engine/depot_tools.dart: -------------------------------------------------------------------------------- 1 | import '../config.dart'; 2 | import '../git.dart'; 3 | import '../logger.dart'; 4 | import '../provider.dart'; 5 | 6 | Future installDepotTools({required Scope scope}) async { 7 | final log = PuroLogger.of(scope); 8 | final config = PuroConfig.of(scope); 9 | final git = GitClient.of(scope); 10 | final depotToolsDir = config.depotToolsDir; 11 | if (depotToolsDir.existsSync() && 12 | depotToolsDir.childFile('gclient').existsSync()) { 13 | log.v('depot_tools already installed'); 14 | } else { 15 | await git.cloneWithProgress( 16 | remote: 17 | 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', 18 | repository: depotToolsDir, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /puro/test/json_grammar_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:puro/src/json_edit/element.dart'; 4 | import 'package:puro/src/json_edit/grammar.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('Special chars', () { 9 | for (final entry in JsonGrammar.escapeChars.entries) { 10 | final result = JsonGrammar.parse('"\\${entry.key}"'); 11 | final value = result.value; 12 | expect(value, isA()); 13 | expect((value as JsonLiteral).value.value, entry.value); 14 | } 15 | }); 16 | 17 | test('Trailing commas', () { 18 | final result = JsonGrammar.parse('{"foo": "bar",}'); 19 | final value = result.value; 20 | expect(jsonEncode(value.toJson()), '{"foo":"bar"}'); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /puro/lib/src/commands/gc.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../env/gc.dart'; 4 | import '../extensions.dart'; 5 | 6 | class GcCommand extends PuroCommand { 7 | @override 8 | final name = 'gc'; 9 | 10 | @override 11 | final description = 'Cleans up unused caches'; 12 | 13 | @override 14 | Future run() async { 15 | final bytes = await collectGarbage( 16 | scope: scope, 17 | maxUnusedCaches: 0, 18 | maxUnusedFlutterTools: 0, 19 | ); 20 | if (bytes == 0) { 21 | return BasicMessageResult('Nothing to clean up'); 22 | } else { 23 | return BasicMessageResult( 24 | 'Cleaned up caches and reclaimed ${bytes.prettyAbbr(metric: true)}B', 25 | ); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /puro/lib/src/commands/build_shell.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../engine/build_env.dart'; 4 | import '../engine/prepare.dart'; 5 | 6 | class BuildShellCommand extends PuroCommand { 7 | @override 8 | final name = 'build-shell'; 9 | 10 | @override 11 | List get aliases => ['build-env', 'buildenv']; 12 | 13 | @override 14 | final description = 15 | 'Starts a shell with the proper environment variables for building the engine'; 16 | 17 | @override 18 | String? get argumentUsage => '[...command]'; 19 | 20 | @override 21 | Future run() async { 22 | final command = unwrapArguments(); 23 | 24 | await prepareEngineSystemDeps(scope: scope); 25 | 26 | final exitCode = await runBuildEnvShell(scope: scope, command: command); 27 | 28 | await runner.exitPuro(exitCode); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /puro/lib/src/downloader.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | import 'package:http/http.dart'; 3 | 4 | import 'http.dart'; 5 | import 'logger.dart'; 6 | import 'progress.dart'; 7 | import 'provider.dart'; 8 | 9 | Future downloadFile({ 10 | required Scope scope, 11 | required Uri url, 12 | required File file, 13 | String? description, 14 | }) async { 15 | final log = PuroLogger.of(scope); 16 | final httpClient = scope.read(clientProvider); 17 | final sink = file.openWrite(); 18 | 19 | log.v('Downloading $url to ${file.path}'); 20 | 21 | await ProgressNode.of(scope).wrap((scope, node) async { 22 | node.description = description ?? 'Downloading ${url.pathSegments.last}'; 23 | final response = await httpClient.send(Request('GET', url)); 24 | HttpException.ensureSuccess(response); 25 | await node.wrapHttpResponse(response).pipe(sink); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /website/overrides/partials/tabs-item.html: -------------------------------------------------------------------------------- 1 | {#- 2 | This file was automatically generated - do not edit 3 | -#} 4 | {% if not class %} 5 | {% set class = "md-tabs__link" %} 6 | {% if nav_item.active %} 7 | {% set class = class ~ " md-tabs__link--active" %} 8 | {% endif %} 9 | {% endif %} 10 | {% if nav_item.children %} 11 | {% set title = title | d(nav_item.title) %} 12 | {% set nav_item = nav_item.children | first %} 13 | {% if nav_item.children %} 14 | {% include "partials/tabs-item.html" %} 15 | {% else %} 16 |
  • 17 | 18 |

    {{ title }}

    19 |
    20 |
  • 21 | {% endif %} 22 | {% else %} 23 |
  • 24 | 25 |

    {{ nav_item.title }}

    26 |
    27 |
  • 28 | {% endif %} 29 | -------------------------------------------------------------------------------- /puro/lib/src/commands/env_rm.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../env/delete.dart'; 4 | 5 | class EnvRmCommand extends PuroCommand { 6 | EnvRmCommand() { 7 | argParser.addFlag( 8 | 'force', 9 | abbr: 'f', 10 | help: 'Delete the environment regardless of whether it is in use', 11 | negatable: false, 12 | ); 13 | } 14 | 15 | @override 16 | final name = 'rm'; 17 | 18 | @override 19 | final description = 'Deletes an environment'; 20 | 21 | @override 22 | String? get argumentUsage => ''; 23 | 24 | @override 25 | Future run() async { 26 | final name = unwrapSingleArgument(); 27 | await deleteEnvironment( 28 | scope: scope, 29 | name: name, 30 | force: argResults!['force'] as bool, 31 | ); 32 | return BasicMessageResult('Deleted environment `$name`'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /puro/lib/src/commands/dart.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | 5 | import '../command.dart'; 6 | import '../command_result.dart'; 7 | import '../env/command.dart'; 8 | import '../env/default.dart'; 9 | 10 | class DartCommand extends PuroCommand { 11 | @override 12 | final name = 'dart'; 13 | 14 | @override 15 | final description = 'Forwards arguments to dart in the current environment'; 16 | 17 | @override 18 | final argParser = ArgParser.allowAnything(); 19 | 20 | @override 21 | String? get argumentUsage => '[...args]'; 22 | 23 | @override 24 | Future run() async { 25 | final environment = await getProjectEnvOrDefault(scope: scope); 26 | final exitCode = await runDartCommand( 27 | scope: scope, 28 | environment: environment, 29 | args: argResults!.arguments, 30 | mode: ProcessStartMode.inheritStdio, 31 | ); 32 | await runner.exitPuro(exitCode); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /puro/lib/src/commands/pub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | 5 | import '../command.dart'; 6 | import '../command_result.dart'; 7 | import '../env/command.dart'; 8 | import '../env/default.dart'; 9 | 10 | class PubCommand extends PuroCommand { 11 | @override 12 | final name = 'pub'; 13 | 14 | @override 15 | final description = 'Forwards arguments to pub in the current environment'; 16 | 17 | @override 18 | final argParser = ArgParser.allowAnything(); 19 | 20 | @override 21 | String? get argumentUsage => '[...args]'; 22 | 23 | @override 24 | Future run() async { 25 | final environment = await getProjectEnvOrDefault(scope: scope); 26 | final exitCode = await runFlutterCommand( 27 | scope: scope, 28 | environment: environment, 29 | args: ['pub', ...argResults!.arguments], 30 | mode: ProcessStartMode.inheritStdio, 31 | ); 32 | await runner.exitPuro(exitCode); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /benchmarks/puro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | start_time=$(date +%s.%3N) 5 | 6 | curl -o- https://puro.dev/install.sh | PURO_VERSION="master" bash 7 | export PATH="$PATH:$HOME/.puro/bin" 8 | 9 | install_time=$(date +%s.%3N) 10 | 11 | puro --no-progress -v create example 3.3.5 12 | 13 | create_time=$(date +%s.%3N) 14 | 15 | puro --no-progress -v -e example flutter --version 16 | 17 | end_time=$(date +%s.%3N) 18 | 19 | install_duration=$(echo "scale=3; $install_time - $start_time" | bc) 20 | create_duration=$(echo "scale=3; $create_time - $install_time" | bc) 21 | run_duration=$(echo "scale=3; $end_time - $create_time" | bc) 22 | total_duration=$(echo "scale=3; $end_time - $start_time" | bc) 23 | echo "Install: ${install_duration}s" 24 | echo "Create: ${create_duration}s" 25 | echo "Run: ${run_duration}s" 26 | echo "Total: ${total_duration}s" 27 | 28 | total_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 29 | echo "Network: ${total_network} bytes" -------------------------------------------------------------------------------- /puro/lib/src/commands/run.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | 5 | import '../command.dart'; 6 | import '../command_result.dart'; 7 | import '../env/command.dart'; 8 | import '../env/default.dart'; 9 | 10 | class RunCommand extends PuroCommand { 11 | @override 12 | final name = 'run'; 13 | 14 | @override 15 | final description = 16 | 'Forwards arguments to dart run in the current environment'; 17 | 18 | @override 19 | final argParser = ArgParser.allowAnything(); 20 | 21 | @override 22 | String? get argumentUsage => '[...args]'; 23 | 24 | @override 25 | Future run() async { 26 | final environment = await getProjectEnvOrDefault(scope: scope); 27 | final exitCode = await runDartCommand( 28 | scope: scope, 29 | environment: environment, 30 | args: ['run', ...argResults!.arguments], 31 | mode: ProcessStartMode.inheritStdio, 32 | ); 33 | await runner.exitPuro(exitCode); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /puro/lib/src/commands/env_ls.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../env/list.dart'; 3 | 4 | class EnvLsCommand extends PuroCommand { 5 | EnvLsCommand() { 6 | argParser.addFlag( 7 | 'projects', 8 | abbr: 'p', 9 | help: 'Whether to show projects using each environment', 10 | negatable: false, 11 | ); 12 | argParser.addFlag( 13 | 'dart', 14 | abbr: 'd', 15 | help: 'Whether to show the dart version of flutter environments', 16 | negatable: false, 17 | ); 18 | } 19 | 20 | @override 21 | final name = 'ls'; 22 | 23 | @override 24 | final description = 25 | 'Lists available environments\nHighlights the current environment with a * and the global environment with a ~'; 26 | 27 | @override 28 | Future run() async { 29 | return listEnvironments( 30 | scope: scope, 31 | showProjects: argResults!['projects'] as bool, 32 | showDartVersion: argResults!['dart'] as bool, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /puro/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: puro 2 | description: A powerful tool for installing and upgrading Flutter versions 3 | version: 1.5.0 4 | repository: https://github.com/pingbird/puro 5 | homepage: https://puro.dev 6 | 7 | environment: 8 | sdk: '>=3.10.0 <4.0.0' 9 | 10 | dependencies: 11 | analyzer: ^8.4.1 12 | args: ^2.3.1 13 | async: ^2.9.0 14 | clock: ^1.1.1 15 | collection: ^1.18.0 16 | dart_console: ^4.1.1 17 | file: ^7.0.0 18 | freezed_annotation: ^3.0.0 19 | http: ^1.1.0 20 | json_annotation: ^4.7.0 21 | meta: ^1.8.0 22 | mutex: ^3.0.0 23 | neoansi: ^0.3.2+1 24 | path: ^1.8.2 25 | petitparser: ^7.0.1 26 | process: ^5.0.0 27 | protobuf: ^6.0.0 28 | pub_semver: ^2.1.1 29 | rxdart: ^0.28.0 30 | stack_trace: ^1.10.0 31 | typed_data: ^1.3.1 32 | vm_service: ^15.0.0 33 | xml: ^6.1.0 34 | yaml: ^3.1.1 35 | yaml_edit: ^2.0.3 36 | 37 | dev_dependencies: 38 | build_runner: ^2.2.1 39 | freezed: ^3.0.6 40 | json_serializable: ^6.4.1 41 | lints: ^6.0.0 42 | test: ^1.16.0 43 | -------------------------------------------------------------------------------- /benchmarks/puro_switching.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | curl -o- https://puro.dev/install.sh | PURO_VERSION="master" bash 5 | export PATH="$PATH:$HOME/.puro/bin" 6 | 7 | puro --no-progress -v create example 3.3.4 8 | puro --no-progress -v rm example 9 | 10 | start_time=$(date +%s.%3N) 11 | start_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 12 | 13 | puro --no-progress -v create example2 3.3.5 14 | 15 | create_time=$(date +%s.%3N) 16 | 17 | puro --no-progress -v -e example2 flutter --version 18 | 19 | end_time=$(date +%s.%3N) 20 | 21 | create_duration=$(echo "scale=3; $create_time - $start_time" | bc) 22 | run_duration=$(echo "scale=3; $end_time - $create_time" | bc) 23 | total_duration=$(echo "scale=3; $end_time - $start_time" | bc) 24 | echo "Create: ${create_duration}s" 25 | echo "Run: ${run_duration}s" 26 | echo "Total: ${total_duration}s" 27 | 28 | end_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 29 | total_network=$(echo "scale=3; $end_network - $start_network" | bc) 30 | echo "Network: ${total_network} bytes" 31 | -------------------------------------------------------------------------------- /puro/lib/src/string_utils.dart: -------------------------------------------------------------------------------- 1 | String escapePowershellString(String str) => str 2 | .replaceAll('`', '``') 3 | .replaceAll('"', '`"') 4 | .replaceAll('\$', '`\$') 5 | .replaceAll('\x00', '`0') 6 | .replaceAll('\x07', '`a') 7 | .replaceAll('\b', '`b') 8 | .replaceAll('\x1b', '`e') 9 | .replaceAll('\f', '`f') 10 | .replaceAll('\n', '`n') 11 | .replaceAll('\r', '`r') 12 | .replaceAll('\t', '`t') 13 | .replaceAll('\v', '`v'); 14 | 15 | String escapeCmdString(String str) => str 16 | .replaceAll('%', '%%') 17 | .replaceAll('&', '^&') 18 | .replaceAll('|', '^|') 19 | .replaceAll('<', '^<') 20 | .replaceAll('>', '^>') 21 | .replaceAll('(', '^(') 22 | .replaceAll(')', '^)') 23 | .replaceAll('!', '^^!') 24 | .replaceAll('"', '^"') 25 | .replaceAll("'", '^\'') 26 | .replaceAll('\x00', '^@') 27 | .replaceAll('\x07', '^G') 28 | .replaceAll('\b', '^H') 29 | .replaceAll('\x1b', '^[') 30 | .replaceAll('\f', '^L') 31 | .replaceAll('\n', '^J') 32 | .replaceAll('\r', '^M') 33 | .replaceAll('\t', '^I') 34 | .replaceAll('\v', '^K'); 35 | -------------------------------------------------------------------------------- /benchmarks/fvm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 5 | 6 | start_time=$(date +%s.%3N) 7 | 8 | brew tap leoafarias/fvm 9 | brew install fvm 10 | 11 | install_time=$(date +%s.%3N) 12 | install_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 13 | 14 | fvm install 3.3.5 15 | 16 | install_flutter_time=$(date +%s.%3N) 17 | 18 | fvm spawn 3.3.5 --version 19 | 20 | end_time=$(date +%s.%3N) 21 | 22 | install_duration=$(echo "scale=3; $install_time - $start_time" | bc) 23 | install_flutter_duration=$(echo "scale=3; $install_flutter_time - $install_time" | bc) 24 | run_duration=$(echo "scale=3; $end_time - $install_flutter_time" | bc) 25 | total_duration=$(echo "scale=3; $end_time - $start_time" | bc) 26 | echo "Install: ${install_duration}s" 27 | echo "Install Flutter: ${install_flutter_duration}s" 28 | echo "Run: ${run_duration}s" 29 | echo "Total: ${total_duration}s" 30 | 31 | total_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 32 | echo "Install Network: ${install_network} bytes" 33 | echo "Total Network: ${total_network} bytes" -------------------------------------------------------------------------------- /benchmarks/puro_cached.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | curl -o- https://puro.dev/install.sh | PURO_VERSION="master" bash 5 | export PATH="$PATH:$HOME/.puro/bin" 6 | 7 | puro --no-progress -v create example 3.3.5 8 | puro --no-progress -v rm example 9 | 10 | echo "==============================================================" 11 | 12 | start_time=$(date +%s.%3N) 13 | start_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 14 | 15 | puro --no-progress -v create example2 3.3.5 16 | 17 | create_time=$(date +%s.%3N) 18 | 19 | puro --no-progress -v -e example2 flutter --version 20 | 21 | end_time=$(date +%s.%3N) 22 | 23 | create_duration=$(echo "scale=3; $create_time - $start_time" | bc) 24 | run_duration=$(echo "scale=3; $end_time - $create_time" | bc) 25 | total_duration=$(echo "scale=3; $end_time - $start_time" | bc) 26 | echo "Create: ${create_duration}s" 27 | echo "Run: ${run_duration}s" 28 | echo "Total: ${total_duration}s" 29 | 30 | end_network=$(cat /proc/net/dev | perl -nle 'm/eth0: *([^ ]*)/; print $1' | tr -d '[:space:]') 31 | total_network=$(echo "scale=3; $end_network - $start_network" | bc) 32 | echo "Network: ${end_network} - ${start_network} = ${total_network} bytes" -------------------------------------------------------------------------------- /website/docs/javascript/os_detect.js: -------------------------------------------------------------------------------- 1 | let me = document.currentScript 2 | let tabbedSet = me.previousElementSibling 3 | let tabs = tabbedSet.querySelectorAll("input") 4 | 5 | // https://stackoverflow.com/a/38241481/2615007 6 | function getOS() { 7 | let userAgent = window.navigator.userAgent, 8 | platform = (window.navigator?.userAgentData?.platform || window.navigator.platform).toLowerCase(), 9 | macosPlatforms = ['macintosh', 'macintel', 'macppc', 'mac68k', 'macos'], 10 | windowsPlatforms = ['win32', 'win64', 'windows', 'wince'], 11 | iosPlatforms = ['iphone', 'ipad', 'ipod'], 12 | os = null; 13 | 14 | if (macosPlatforms.indexOf(platform) !== -1) { 15 | return 'Mac'; 16 | } else if (iosPlatforms.indexOf(platform) !== -1) { 17 | return 'iOS'; 18 | } else if (windowsPlatforms.indexOf(platform) !== -1) { 19 | return 'Windows'; 20 | } else if (/android/.test(userAgent)) { 21 | return 'Android'; 22 | } else if (/linux/.test(platform)) { 23 | return 'Linux'; 24 | } 25 | } 26 | 27 | switch (getOS()) { 28 | case 'Linux': 29 | tabs[1].click() 30 | break; 31 | case 'Mac': 32 | tabs[2].click() 33 | break; 34 | } -------------------------------------------------------------------------------- /puro/lib/src/commands/prefs.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../config.dart'; 4 | import '../terminal.dart'; 5 | 6 | class PrefsCommand extends PuroCommand { 7 | @override 8 | final name = '_prefs'; 9 | 10 | @override 11 | final description = 'Manages hidden configuration settings'; 12 | 13 | @override 14 | String? get argumentUsage => ' [value]'; 15 | 16 | @override 17 | Future run() async { 18 | final args = unwrapArguments(atMost: 2); 19 | final config = PuroConfig.of(scope); 20 | if (args.isEmpty) { 21 | final prefs = await readGlobalPrefs(scope: scope); 22 | return BasicMessageResult( 23 | prettyJsonEncoder.convert(prefs.toProto3Json()), 24 | type: CompletionType.info, 25 | ); 26 | } 27 | final vars = PuroInternalPrefsVars(scope: scope, config: config); 28 | if (args.length > 1) { 29 | await vars.writeVar(args[0], args[1]); 30 | return BasicMessageResult('Updated ${args[0]}'); 31 | } else { 32 | final dynamic value = await vars.readVar(args[0]); 33 | return BasicMessageResult( 34 | '${args[0]} = ${prettyJsonEncoder.convert(value)}', 35 | type: CompletionType.info, 36 | ); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /installer/windows/install.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define AppName "Puro" 5 | #define AppURL "https://puro.dev" 6 | #define AppExeName "puro.exe" 7 | 8 | [Setup] 9 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 10 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 11 | AppId={{E5B42C4E-7A2D-4D17-9769-8F82B523BE82} 12 | AppName={#AppName} 13 | AppVersion={#AppVersion} 14 | AppPublisherURL={#AppURL} 15 | AppSupportURL={#AppURL} 16 | AppUpdatesURL={#AppURL} 17 | DefaultDirName={%USERPROFILE}\.puro 18 | DefaultGroupName={#AppName} 19 | DisableProgramGroupPage=yes 20 | DisableDirPage=No 21 | LicenseFile=..\..\puro\LICENSE 22 | ; Remove the following line to run in administrative install mode (install for all users.) 23 | PrivilegesRequired=lowest 24 | PrivilegesRequiredOverridesAllowed=commandline 25 | OutputBaseFilename=puro_installer 26 | SetupIconFile=puro.ico 27 | Compression=lzma 28 | SolidCompression=yes 29 | WizardStyle=modern 30 | ChangesEnvironment=true 31 | 32 | [Languages] 33 | Name: "english"; MessagesFile: "compiler:Default.isl" 34 | 35 | [Files] 36 | Source: "..\..\puro\bin\puro.exe"; DestDir: "{app}\bin"; Flags: ignoreversion 37 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 38 | 39 | [Run] 40 | Filename: "{app}\bin\{#AppExeName}"; Parameters: "install-puro --root=."; WorkingDir: "{app}" 41 | 42 | [Icons] 43 | Name: "{group}\{#AppName}"; Filename: "{app}\bin\{#AppExeName}" 44 | -------------------------------------------------------------------------------- /puro/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 ping undefined 2 | 3 | Copyright 2014 The Flutter Authors. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /website/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Puro 2 | site_url: https://puro.dev 3 | nav: 4 | - Home: 5 | - index.md 6 | - Reference: 7 | - reference/manual.md 8 | - reference/commands.md 9 | - reference/changelog.md 10 | - 'pub.dev': https://pub.dev/packages/puro 11 | theme: 12 | name: material 13 | custom_dir: overrides 14 | logo: assets/puro_icon_small.png 15 | favicon: assets/puro_icon_small.png 16 | palette: 17 | - scheme: slate 18 | primary: deep purple 19 | accent: deep purple 20 | toggle: 21 | icon: material/toggle-switch-off-outline 22 | name: Switch to light mode 23 | - scheme: default 24 | primary: deep purple 25 | accent: deep purple 26 | toggle: 27 | icon: material/toggle-switch 28 | name: Switch to dark mode 29 | features: 30 | - navigation.tabs 31 | - navigation.instant 32 | - toc.follow 33 | - content.code.annotate 34 | markdown_extensions: 35 | - pm_attr_list:PMAttrListExtension 36 | - tables 37 | - admonition 38 | - pymdownx.highlight: 39 | anchor_linenums: true 40 | - pymdownx.emoji: 41 | emoji_index: !!python/name:materialx.emoji.twemoji 42 | emoji_generator: !!python/name:materialx.emoji.to_svg 43 | - pymdownx.inlinehilite 44 | - pymdownx.snippets 45 | - pymdownx.superfences 46 | - pymdownx.tabbed: 47 | alternate_style: true 48 | extra: 49 | generator: false 50 | analytics: 51 | provider: google 52 | property: G-245TJ8036B 53 | social: 54 | - icon: fontawesome/solid/heart 55 | link: https://tst.sh 56 | repo_url: https://github.com/pingbird/puro 57 | extra_css: 58 | - stylesheets/extra.css 59 | -------------------------------------------------------------------------------- /puro/lib/src/commands/version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../command.dart'; 4 | import '../command_result.dart'; 5 | import '../install/profile.dart'; 6 | import '../terminal.dart'; 7 | import '../version.dart'; 8 | 9 | class VersionCommand extends PuroCommand { 10 | VersionCommand() { 11 | argParser.addFlag( 12 | 'plain', 13 | negatable: false, 14 | help: 'Print just the version to stdout and exit', 15 | ); 16 | argParser.addFlag('release', negatable: false, hide: true); 17 | } 18 | 19 | @override 20 | String get name => 'version'; 21 | 22 | @override 23 | String get description => 'Prints version information'; 24 | 25 | @override 26 | bool get allowUpdateCheck => false; 27 | 28 | @override 29 | Future run() async { 30 | final plain = argResults!['plain'] as bool; 31 | final puroVersion = await PuroVersion.of(scope); 32 | if (plain) { 33 | Terminal.of(scope).flushStatus(); 34 | await stderr.flush(); 35 | stdout.write('${puroVersion.semver}'); 36 | await runner.exitPuro(0); 37 | } 38 | final externalMessage = await detectExternalFlutterInstallations( 39 | scope: scope, 40 | ); 41 | final updateMessage = await checkIfUpdateAvailable( 42 | scope: scope, 43 | runner: runner, 44 | alwaysNotify: true, 45 | ); 46 | return BasicMessageResult.list([ 47 | if (externalMessage != null) externalMessage, 48 | if (updateMessage != null) updateMessage, 49 | CommandMessage( 50 | 'Puro ${puroVersion.semver} ' 51 | '(${puroVersion.type.name}/${puroVersion.target.name})\n' 52 | 'Dart ${Platform.version}', 53 | type: CompletionType.info, 54 | ), 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /installer/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "${PURO_ROOT-}" ]; then 4 | PURO_ROOT=${HOME}/.puro 5 | fi 6 | 7 | if [ -z "${PURO_VERSION-}" ]; then 8 | PURO_VERSION="master" 9 | fi 10 | 11 | PURO_BIN="$PURO_ROOT/bin" 12 | PURO_EXE="$PURO_BIN/puro.new" 13 | 14 | is_sourced() { 15 | if [ -n "$ZSH_VERSION" ]; then 16 | case $ZSH_EVAL_CONTEXT in *:file:*) return 0;; esac 17 | else # Add additional POSIX-compatible shell names here, if needed. 18 | case ${0##*/} in dash|-dash|bash|-bash|ksh|-ksh|sh|-sh) return 0;; esac 19 | fi 20 | return 1 # NOT sourced. 21 | } 22 | 23 | if is_sourced; then 24 | # shellcheck disable=SC2209 25 | ret=return 26 | else 27 | # shellcheck disable=SC2209 28 | # shellcheck disable=SC2034 29 | ret=exit 30 | fi 31 | 32 | OS="$(uname -s)" 33 | ARCH="$(uname -m)" 34 | if [ "$OS" = 'Darwin' ]; then 35 | if [ "$ARCH" = 'arm64' ]; then 36 | DOWNLOAD_URL="https://puro.dev/builds/${PURO_VERSION}/darwin-arm64/puro" 37 | else 38 | DOWNLOAD_URL="https://puro.dev/builds/${PURO_VERSION}/darwin-x64/puro" 39 | fi 40 | elif [ "$OS" = 'Linux' ]; then 41 | DOWNLOAD_URL="https://puro.dev/builds/${PURO_VERSION}/linux-x64/puro" 42 | else 43 | >&2 echo "Error: Unknown OS: $OS" 44 | $ret 1 45 | fi 46 | 47 | command -v curl > /dev/null 2>&1 || { 48 | >&2 echo 'Error: could not find curl command' 49 | case "$OS" in 50 | Darwin) 51 | >&2 echo 'Consider running "brew install curl".' 52 | ;; 53 | Linux) 54 | >&2 echo 'Consider running "sudo apt-get install curl".' 55 | ;; 56 | esac 57 | $ret 1 58 | } 59 | 60 | mkdir -p "$PURO_BIN" 61 | curl -f --retry 3 --output "$PURO_EXE" "$DOWNLOAD_URL" || { 62 | >&2 echo "Error downloading $DOWNLOAD_URL" 63 | $ret $? 64 | } 65 | chmod +x "$PURO_EXE" || $ret $? 66 | 67 | "$PURO_EXE" install-puro --promote -------------------------------------------------------------------------------- /puro/lib/src/engine/dart_version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import '../config.dart'; 5 | import '../env/create.dart'; 6 | import '../git.dart'; 7 | import '../http.dart'; 8 | import '../provider.dart'; 9 | 10 | Future getEngineDartCommit({ 11 | required Scope scope, 12 | required String engineCommit, 13 | }) async { 14 | final config = PuroConfig.of(scope); 15 | final git = GitClient.of(scope); 16 | final http = scope.read(clientProvider); 17 | final sharedRepository = config.sharedEngineDir; 18 | 19 | String parseDEPS(Uint8List result) { 20 | final lines = utf8.decode(result).split('\n'); 21 | for (final line in lines) { 22 | if (line.startsWith(' "dart_revision":')) { 23 | return line.split('"')[3]; 24 | } 25 | } 26 | throw AssertionError('Failed to parse DEPS for $engineCommit'); 27 | } 28 | 29 | if (sharedRepository.existsSync()) { 30 | var result = await git.tryCat(repository: sharedRepository, path: 'DEPS'); 31 | if (result != null) { 32 | return parseDEPS(result); 33 | } 34 | await git.fetch(repository: sharedRepository); 35 | result = await git.tryCat(repository: sharedRepository, path: 'DEPS'); 36 | if (result != null) { 37 | return parseDEPS(result); 38 | } 39 | } 40 | 41 | final depsUrl = config.tryGetEngineGitDownloadUrl( 42 | commit: engineCommit, 43 | path: 'DEPS', 44 | ); 45 | 46 | if (depsUrl != null) { 47 | final response = await http.get(depsUrl); 48 | HttpException.ensureSuccess(response); 49 | return parseDEPS(response.bodyBytes); 50 | } 51 | 52 | await fetchOrCloneShared( 53 | scope: scope, 54 | repository: sharedRepository, 55 | remoteUrl: config.engineGitUrl, 56 | ); 57 | 58 | // Try again after cloning the repository 59 | return getEngineDartCommit(scope: scope, engineCommit: engineCommit); 60 | } 61 | -------------------------------------------------------------------------------- /puro/lib/src/install/upgrade.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../config.dart'; 4 | import '../downloader.dart'; 5 | import '../extensions.dart'; 6 | import '../http.dart'; 7 | import '../logger.dart'; 8 | import '../process.dart'; 9 | import '../provider.dart'; 10 | import '../terminal.dart'; 11 | 12 | Future upgradePuro({ 13 | required Scope scope, 14 | required String targetVersion, 15 | required bool? path, 16 | }) async { 17 | final config = PuroConfig.of(scope); 18 | final terminal = Terminal.of(scope); 19 | final log = PuroLogger.of(scope); 20 | final buildTarget = config.buildTarget; 21 | final tempFile = config.puroExecutableTempFile; 22 | 23 | tempFile.parent.createSync(recursive: true); 24 | await downloadFile( 25 | scope: scope, 26 | url: config.puroBuildsUrl.append( 27 | path: 28 | '$targetVersion/' 29 | '${buildTarget.name}/' 30 | '${buildTarget.executableName}', 31 | ), 32 | file: tempFile, 33 | description: 'Downloading puro $targetVersion', 34 | ); 35 | if (!Platform.isWindows) { 36 | await runProcess(scope, 'chmod', ['+x', '--', tempFile.path]); 37 | } 38 | config.puroExecutableFile.deleteOrRenameSync(); 39 | tempFile.renameSync(config.puroExecutableFile.path); 40 | 41 | terminal.flushStatus(); 42 | final installProcess = 43 | await startProcess(scope, config.puroExecutableFile.path, [ 44 | if (terminal.enableColor) '--color', 45 | if (terminal.enableStatus) '--progress', 46 | '--log-level=${log.level?.index ?? 0}', 47 | 'install-puro', 48 | if (path != null) 49 | if (path) '--path' else '--no-path', 50 | ]); 51 | final stdoutFuture = installProcess.stdout 52 | .listen(stdout.add) 53 | .asFuture(); 54 | await installProcess.stderr.listen(stderr.add).asFuture(); 55 | await stdoutFuture; 56 | return installProcess.exitCode; 57 | } 58 | -------------------------------------------------------------------------------- /puro/lib/src/commands/flutter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | 5 | import '../command.dart'; 6 | import '../command_result.dart'; 7 | import '../env/command.dart'; 8 | import '../env/default.dart'; 9 | import '../logger.dart'; 10 | import '../terminal.dart'; 11 | 12 | class FlutterCommand extends PuroCommand { 13 | @override 14 | final name = 'flutter'; 15 | 16 | @override 17 | final description = 18 | 'Forwards arguments to flutter in the current environment'; 19 | 20 | @override 21 | final argParser = ArgParser.allowAnything(); 22 | 23 | @override 24 | String? get argumentUsage => '[...args]'; 25 | 26 | @override 27 | Future run() async { 28 | final log = PuroLogger.of(scope); 29 | final environment = await getProjectEnvOrDefault(scope: scope); 30 | log.v('Flutter SDK: ${environment.flutter.sdkDir.path}'); 31 | final nonOptionArgs = argResults!.arguments 32 | .where((e) => !e.startsWith('-')) 33 | .toList(); 34 | if (nonOptionArgs.isNotEmpty) { 35 | if (nonOptionArgs.first == 'upgrade') { 36 | runner.addMessage( 37 | 'Using puro to upgrade flutter', 38 | type: CompletionType.info, 39 | ); 40 | return (await runner.run(['upgrade', environment.name]))!; 41 | } else if (nonOptionArgs.first == 'channel' && nonOptionArgs.length > 1) { 42 | runner.addMessage( 43 | 'Using puro to switch flutter channel', 44 | type: CompletionType.info, 45 | ); 46 | return (await runner.run(['upgrade', unwrapArguments(exactly: 2)[1]]))!; 47 | } 48 | } 49 | final exitCode = await runFlutterCommand( 50 | scope: scope, 51 | environment: environment, 52 | args: argResults!.arguments, 53 | // inheritStdio is useful because it allows Flutter to detect the 54 | // terminal, otherwise it won't show any colors. 55 | mode: ProcessStartMode.inheritStdio, 56 | ); 57 | exit(exitCode); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /website/overrides/partials/comments.html: -------------------------------------------------------------------------------- 1 | {% if page.meta.comments %} 2 |

    {{ lang.t("meta.comments") }}

    3 | 18 | 19 | 20 | 48 | {% endif %} -------------------------------------------------------------------------------- /puro/lib/src/commands/env_create.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../config.dart'; 4 | import '../env/create.dart'; 5 | import '../env/default.dart'; 6 | import '../env/releases.dart'; 7 | import '../env/version.dart'; 8 | import '../install/bin.dart'; 9 | 10 | class EnvCreateCommand extends PuroCommand { 11 | EnvCreateCommand() { 12 | argParser.addOption( 13 | 'channel', 14 | help: 15 | 'The Flutter channel, in case multiple channels have builds with the same version number.', 16 | valueHelp: 'name', 17 | ); 18 | argParser.addOption( 19 | 'fork', 20 | help: 21 | 'The origin to use when cloning the framework, puro will set the upstream automatically.', 22 | valueHelp: 'url', 23 | ); 24 | } 25 | 26 | @override 27 | final name = 'create'; 28 | 29 | @override 30 | String? get argumentUsage => ' [version]'; 31 | 32 | @override 33 | final description = 'Sets up a new Flutter environment'; 34 | 35 | @override 36 | Future run() async { 37 | final channel = argResults!['channel'] as String?; 38 | final fork = argResults!['fork'] as String?; 39 | final args = unwrapArguments(atLeast: 1, atMost: 2); 40 | final version = args.length > 1 ? args[1] : null; 41 | final envName = args.first.toLowerCase(); 42 | ensureValidEnvName(envName); 43 | 44 | await ensurePuroInstalled(scope: scope); 45 | 46 | if (fork != null) { 47 | if (pseudoEnvironmentNames.contains(envName) || isValidVersion(envName)) { 48 | throw CommandError( 49 | 'Cannot create fixed version `$envName` with a fork', 50 | ); 51 | } 52 | return createEnvironment( 53 | scope: scope, 54 | envName: envName, 55 | forkRemoteUrl: fork, 56 | forkRef: version, 57 | ); 58 | } else { 59 | return createEnvironment( 60 | scope: scope, 61 | envName: envName, 62 | flutterVersion: await FlutterVersion.query( 63 | scope: scope, 64 | version: version, 65 | channel: channel, 66 | defaultVersion: isPseudoEnvName(envName) ? envName : 'stable', 67 | ), 68 | ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /puro/lib/src/engine/build_env.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:neoansi/neoansi.dart'; 4 | 5 | import '../command_result.dart'; 6 | import '../config.dart'; 7 | import '../process.dart'; 8 | import '../provider.dart'; 9 | import '../terminal.dart'; 10 | import 'worker.dart'; 11 | 12 | Future runBuildEnvShell({ 13 | required Scope scope, 14 | List? command, 15 | EnvConfig? environment, 16 | }) async { 17 | final terminal = Terminal.of(scope); 18 | 19 | if (environment != null) { 20 | environment.engine.ensureExists(); 21 | } 22 | 23 | if (Platform.environment['PURO_ENGINE_BUILD_ENV'] != null) { 24 | throw CommandError('Already inside an engine build environment'); 25 | } 26 | 27 | final buildEnv = await getEngineBuildEnvVars(scope: scope); 28 | final defaultShell = command == null || command.isEmpty; 29 | if (defaultShell) { 30 | if (Platform.isWindows) { 31 | final processTree = await getParentProcesses(scope: scope); 32 | command = ['cmd.exe']; 33 | for (final process in processTree) { 34 | if (process.name == 'powershell.exe' || process.name == 'cmd.exe') { 35 | command = [process.name]; 36 | break; 37 | } 38 | } 39 | } else { 40 | final shell = Platform.environment['SHELL']; 41 | if (shell != null && shell.isNotEmpty) { 42 | command = [shell]; 43 | } else { 44 | command = [if (Platform.isMacOS) '/bin/zsh' else '/bin/bash']; 45 | } 46 | } 47 | 48 | terminal 49 | ..flushStatus() 50 | ..writeln( 51 | terminal.format.color( 52 | '[ Running ${command![0]} with engine build environment,\n' 53 | ' type `exit` to return to the normal shell ]\n', 54 | bold: true, 55 | foregroundColor: Ansi8BitColor.blue, 56 | ), 57 | ); 58 | } 59 | 60 | final process = await startProcess( 61 | scope, 62 | command.first, 63 | command.skip(1).toList(), 64 | environment: buildEnv, 65 | mode: ProcessStartMode.inheritStdio, 66 | workingDirectory: defaultShell ? environment?.engine.srcDir.path : null, 67 | rosettaWorkaround: true, 68 | ); 69 | 70 | final exitCode = await process.exitCode; 71 | 72 | if (defaultShell) { 73 | terminal 74 | ..flushStatus() 75 | ..writeln( 76 | terminal.format.color( 77 | '\n[ Returning from engine build shell ]', 78 | bold: true, 79 | foregroundColor: Ansi8BitColor.blue, 80 | ), 81 | ); 82 | } 83 | 84 | return exitCode; 85 | } 86 | -------------------------------------------------------------------------------- /puro/lib/src/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:clock/clock.dart'; 4 | 5 | /// Debounces an asynchronous operation, calling [onUpdate] after [add] is used 6 | /// to update [value]. 7 | /// 8 | /// This class makes a few useful guarantees: 9 | /// 10 | /// 1. [onUpdate] will not be called more frequent than [minDuration]. 11 | /// 2. [onUpdate] will be called at least every [maxDuration] if [add] is called 12 | /// more frequent than [minDuration]. 13 | /// 3. [onUpdate] will not be called concurrently, e.g. if [add] is called while 14 | /// an asynchronous [onUpdate] is in progress, the debouncer will wait until 15 | /// it finishes. 16 | class Debouncer { 17 | Debouncer({ 18 | required this.minDuration, 19 | this.maxDuration, 20 | required this.onUpdate, 21 | T? initialValue, 22 | }) { 23 | if (initialValue is T) { 24 | _value = initialValue; 25 | } 26 | } 27 | 28 | final Duration minDuration; 29 | final Duration? maxDuration; 30 | final FutureOr Function(T value) onUpdate; 31 | 32 | late T _value; 33 | T get value => _value; 34 | 35 | Timer? _timer; 36 | var _isUpdating = false; 37 | var _shouldUpdate = false; 38 | DateTime? _lastUpdate; 39 | 40 | Future _update({DateTime? now}) async { 41 | _timer?.cancel(); 42 | _timer = null; 43 | if (_isUpdating) { 44 | _shouldUpdate = true; 45 | return; 46 | } 47 | _lastUpdate = now ?? clock.now(); 48 | _isUpdating = true; 49 | try { 50 | await onUpdate(_value); 51 | } catch (exception, stackTrace) { 52 | Zone.current.handleUncaughtError(exception, stackTrace); 53 | } 54 | _isUpdating = false; 55 | if (_shouldUpdate) { 56 | _shouldUpdate = false; 57 | unawaited(_update()); 58 | } 59 | } 60 | 61 | void add(T value) { 62 | _value = value; 63 | 64 | _timer?.cancel(); 65 | _timer = null; 66 | 67 | final now = clock.now(); 68 | _lastUpdate ??= now; 69 | 70 | var duration = minDuration; 71 | if (maxDuration != null) { 72 | final newDuration = _lastUpdate!.add(maxDuration!).difference(now); 73 | if (newDuration < duration) { 74 | duration = newDuration; 75 | } 76 | } 77 | 78 | if (duration.isNegative || duration == Duration.zero) { 79 | _update(now: now); 80 | } else { 81 | _timer = Timer(duration, _update); 82 | } 83 | } 84 | 85 | void reset(T value) { 86 | _timer?.cancel(); 87 | _timer = null; 88 | _shouldUpdate = false; 89 | _value = value; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /puro/lib/src/env/delete.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../command_result.dart'; 4 | import '../config.dart'; 5 | import '../process.dart'; 6 | import '../provider.dart'; 7 | import '../terminal.dart'; 8 | 9 | Future ensureNoProjectsUsingEnv({ 10 | required Scope scope, 11 | required EnvConfig environment, 12 | }) async { 13 | final config = PuroConfig.of(scope); 14 | final dotfiles = await getDotfilesUsingEnv( 15 | scope: scope, 16 | environment: environment, 17 | ); 18 | if (dotfiles.isNotEmpty) { 19 | throw CommandError.list([ 20 | CommandMessage( 21 | 'Environment `${environment.name}` is currently used by the following ' 22 | 'projects:\n${dotfiles.map((p) => '* ${config.shortenHome(p.parent.path)}').join('\n')}', 23 | ), 24 | CommandMessage( 25 | 'Pass `-f` to ignore this warning', 26 | type: CompletionType.info, 27 | ), 28 | ]); 29 | } 30 | } 31 | 32 | /// Deletes an environment. 33 | Future deleteEnvironment({ 34 | required Scope scope, 35 | required String name, 36 | required bool force, 37 | }) async { 38 | final config = PuroConfig.of(scope); 39 | final env = config.getEnv(name); 40 | env.ensureExists(); 41 | 42 | if (!force) { 43 | await ensureNoProjectsUsingEnv(scope: scope, environment: env); 44 | } 45 | 46 | // Try deleting the lock file first so we don't delete an env being updated 47 | if (env.updateLockFile.existsSync()) { 48 | await env.updateLockFile.delete(); 49 | } 50 | 51 | try { 52 | await env.envDir.delete(recursive: true); 53 | } catch (e) { 54 | if (!env.flutter.cache.dartSdk.dartExecutable.existsSync()) { 55 | rethrow; 56 | } 57 | 58 | // Try killing dart processes that might be preventing us from deleting the 59 | // environment. 60 | if (Platform.isWindows) { 61 | await runProcess(scope, 'wmic', [ 62 | 'process', 63 | 'where', 64 | 'path="${env.flutter.cache.dartSdk.dartExecutable.resolveSymbolicLinksSync().replaceAll('\\', '\\\\')}"', 65 | 'delete', 66 | ]); 67 | } else { 68 | final result = await runProcess(scope, 'pgrep', [ 69 | '-f', 70 | env.flutter.cache.dartSdk.dartExecutable.path, 71 | ]); 72 | final pids = (result.stdout as String).trim().split(RegExp('\\s+')); 73 | await runProcess(scope, 'kill', ['-9', ...pids]); 74 | } 75 | 76 | // Wait a bit for the handles to be released. 77 | await Future.delayed(const Duration(seconds: 2)); 78 | 79 | // Try deleting again. 80 | await env.envDir.delete(recursive: true); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /puro/lib/src/commands/env_upgrade.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../config.dart'; 4 | import '../env/releases.dart'; 5 | import '../env/upgrade.dart'; 6 | import '../env/version.dart'; 7 | 8 | class EnvUpgradeCommand extends PuroCommand { 9 | EnvUpgradeCommand() { 10 | argParser.addOption( 11 | 'channel', 12 | help: 13 | 'The Flutter channel, in case multiple channels have builds with the same version number.', 14 | valueHelp: 'name', 15 | ); 16 | argParser.addFlag( 17 | 'force', 18 | help: 'Forcefully upgrade the framework, erasing any unstaged changes', 19 | negatable: false, 20 | ); 21 | } 22 | 23 | @override 24 | final name = 'upgrade'; 25 | 26 | @override 27 | List get aliases => ['downgrade']; 28 | 29 | @override 30 | String? get argumentUsage => ' [version]'; 31 | 32 | @override 33 | final description = 34 | 'Upgrades or downgrades an environment to a new version of Flutter'; 35 | 36 | @override 37 | Future run() async { 38 | final config = PuroConfig.of(scope); 39 | final channel = argResults!['channel'] as String?; 40 | final force = argResults!['force'] as bool; 41 | final args = unwrapArguments(atLeast: 1, atMost: 2); 42 | var version = args.length > 1 ? args[1] : null; 43 | 44 | final environment = config.getEnv(args[0]); 45 | 46 | if (!environment.exists && args[0].toLowerCase() == 'puro') { 47 | throw CommandError( 48 | 'Environment `$name` does not exist\n' 49 | 'Did you mean to run `puro upgrade-puro`?', 50 | ); 51 | } 52 | environment.ensureExists(); 53 | 54 | if (version == null && channel == null) { 55 | final prefs = await environment.readPrefs(scope: scope); 56 | if (prefs.hasDesiredVersion()) { 57 | final versionModel = prefs.desiredVersion; 58 | if (versionModel.hasBranch()) { 59 | version = prefs.desiredVersion.branch; 60 | } 61 | } 62 | } 63 | 64 | if (version == null && channel == null) { 65 | if (pseudoEnvironmentNames.contains(environment.name)) { 66 | version = environment.name; 67 | } else { 68 | throw CommandError( 69 | 'No version provided and environment `${environment.name}` is not on a branch', 70 | ); 71 | } 72 | } 73 | 74 | return upgradeEnvironment( 75 | scope: scope, 76 | environment: environment, 77 | toVersion: await FlutterVersion.query( 78 | scope: scope, 79 | version: version, 80 | channel: channel, 81 | ), 82 | force: force, 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /puro/proto/puro.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message CommandErrorModel { 4 | string exception = 1; 5 | string exceptionType = 2; 6 | string stackTrace = 3; 7 | } 8 | 9 | message LogEntryModel { 10 | string timestamp = 1; 11 | int32 level = 2; 12 | string message = 3; 13 | } 14 | 15 | message FlutterVersionModel { 16 | string commit = 1; 17 | optional string version = 2; 18 | optional string branch = 3; 19 | optional string tag = 4; 20 | } 21 | 22 | message EnvironmentInfoModel { 23 | string name = 1; 24 | string path = 2; 25 | optional FlutterVersionModel version = 3; 26 | optional string dartVersion = 5; 27 | repeated string projects = 4; 28 | } 29 | 30 | message EnvironmentListModel { 31 | repeated EnvironmentInfoModel environments = 1; 32 | optional string projectEnvironment = 2; 33 | optional string globalEnvironment = 3; 34 | } 35 | 36 | message EnvironmentUpgradeModel { 37 | string name = 1; 38 | FlutterVersionModel from = 2; 39 | FlutterVersionModel to = 3; 40 | } 41 | 42 | message CommandMessageModel { 43 | string type = 1; 44 | string message = 2; 45 | } 46 | 47 | message CommandResultModel { 48 | bool success = 1; 49 | repeated CommandMessageModel messages = 2; 50 | optional string usage = 3; 51 | optional CommandErrorModel error = 4; 52 | repeated LogEntryModel logs = 5; 53 | optional EnvironmentListModel environmentList = 6; 54 | optional EnvironmentUpgradeModel environmentUpgrade = 7; 55 | } 56 | 57 | message PuroGlobalPrefsModel { 58 | optional string defaultEnvironment = 1; 59 | optional string lastUpdateCheck = 2; 60 | optional string lastUpdateNotification = 3; 61 | optional string lastUpdateNotificationCommand = 8; 62 | optional bool enableUpdateCheck = 4; 63 | optional bool enableProfileUpdate = 5; 64 | optional string profileOverride = 6; 65 | repeated string projectDotfiles = 7; 66 | optional string pubCacheDir = 9; 67 | optional string flutterGitUrl = 10; 68 | optional string engineGitUrl = 11; 69 | optional string dartSdkGitUrl = 12; 70 | optional string releasesJsonUrl = 13; 71 | optional string flutterStorageBaseUrl = 14; 72 | optional string puroBuildsUrl = 15; 73 | optional string puroBuildTarget = 16; 74 | optional bool shouldInstall = 18; 75 | optional bool legacyPubCache = 19; 76 | } 77 | 78 | message PuroEnvPrefsModel { 79 | optional FlutterVersionModel desiredVersion = 1; 80 | optional string forkRemoteUrl = 2; 81 | optional string engineForkRemoteUrl = 3; 82 | optional bool precompileTool = 4; 83 | optional bool patched = 5; 84 | } 85 | 86 | message PuroDotfileModel { 87 | optional string env = 1; 88 | optional string previousDartSdk = 2; 89 | optional string previousFlutterSdk = 3; 90 | } 91 | -------------------------------------------------------------------------------- /puro/lib/src/env/rename.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:file/file.dart'; 4 | 5 | import '../../models.dart'; 6 | import '../command_result.dart'; 7 | import '../config.dart'; 8 | import '../logger.dart'; 9 | import '../progress.dart'; 10 | import '../provider.dart'; 11 | import '../terminal.dart'; 12 | import '../workspace/install.dart'; 13 | import 'default.dart'; 14 | 15 | /// Deletes an environment. 16 | Future renameEnvironment({ 17 | required Scope scope, 18 | required String name, 19 | required String newName, 20 | }) async { 21 | final config = PuroConfig.of(scope); 22 | final env = config.getEnv(name); 23 | final log = PuroLogger.of(scope); 24 | env.ensureExists(); 25 | final newEnv = config.getEnv(newName); 26 | 27 | if (newEnv.exists) { 28 | throw CommandError('Environment `$newName` already exists'); 29 | } else if (env.name == newEnv.name) { 30 | throw CommandError('Environment `$name` is already named `$newName`'); 31 | } else if (isPseudoEnvName(newName)) { 32 | throw CommandError( 33 | 'Environment `$newName` is already pinned to a version, use `puro create $newName` to create it', 34 | ); 35 | } 36 | 37 | final dotfiles = await getDotfilesUsingEnv(scope: scope, environment: env); 38 | 39 | if (env.updateLockFile.existsSync()) { 40 | await env.updateLockFile.delete(); 41 | } 42 | await env.envDir.rename(newEnv.envDir.path); 43 | 44 | final updated = []; 45 | 46 | await ProgressNode.of(scope).wrap((scope, node) async { 47 | node.description = 'Updating projects'; 48 | for (final dotfile in dotfiles) { 49 | try { 50 | await switchEnvironment( 51 | scope: scope, 52 | envName: newName, 53 | projectConfig: ProjectConfig( 54 | parentConfig: config, 55 | projectDir: dotfile.parent, 56 | parentProjectDir: dotfile.parent, 57 | ), 58 | passive: true, 59 | ); 60 | updated.add(dotfile); 61 | } catch (exception, stackTrace) { 62 | log.e('Exception while switching environment of ${dotfile.parent}'); 63 | log.e('$exception\n$stackTrace'); 64 | } 65 | final data = jsonDecode(dotfile.readAsStringSync()); 66 | final model = PuroDotfileModel.create(); 67 | model.mergeFromProto3Json(data); 68 | model.env = newName; 69 | dotfile.writeAsStringSync( 70 | prettyJsonEncoder.convert(model.toProto3Json()), 71 | ); 72 | } 73 | }); 74 | 75 | if (dotfiles.isNotEmpty) { 76 | CommandMessage( 77 | 'Updated the following projects:\n' 78 | '${dotfiles.map((p) => '* ${p.parent.path}').join('\n')}', 79 | type: CompletionType.info, 80 | ).queue(scope); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /puro/lib/src/ast/reader.dart: -------------------------------------------------------------------------------- 1 | // This is a work in progress 2 | 3 | import 'dart:typed_data'; 4 | 5 | class Reader { 6 | Reader(this._bytes); 7 | 8 | final Uint8List _bytes; 9 | late final byteData = _bytes.buffer.asByteData( 10 | _bytes.offsetInBytes, 11 | _bytes.lengthInBytes, 12 | ); 13 | var byteOffset = 0; 14 | int readByte() => _bytes[byteOffset++]; 15 | int peekByte() => _bytes[byteOffset]; 16 | 17 | late final componentFileSize = byteData.getUint32(_bytes.length - 4); 18 | late final libraryCount = byteData.getUint32(_bytes.length - 8) + 1; 19 | late final libraryOffsetsStartOffset = _bytes.length - 8 - libraryCount * 4; 20 | late final List libraryOffsets = List.generate( 21 | libraryCount, 22 | (i) => byteData.getUint32(libraryOffsetsStartOffset + i * 4), 23 | ); 24 | late final magic = byteData.getUint32(0); 25 | late final formatVersion = byteData.getUint32(4); 26 | 27 | int readUInt30() { 28 | final int byte = readByte(); 29 | if (byte & 0x80 == 0) { 30 | // 0xxxxxxx 31 | return byte; 32 | } else if (byte & 0x40 == 0) { 33 | // 10xxxxxx 34 | return ((byte & 0x3F) << 8) | readByte(); 35 | } else { 36 | // 11xxxxxx 37 | return ((byte & 0x3F) << 24) | 38 | (readByte() << 16) | 39 | (readByte() << 8) | 40 | readByte(); 41 | } 42 | } 43 | 44 | int readUInt32() { 45 | return (readByte() << 24) | 46 | (readByte() << 16) | 47 | (readByte() << 8) | 48 | readByte(); 49 | } 50 | 51 | final _doubleBuffer = Float64List(1); 52 | Uint8List? _doubleBufferUint8; 53 | 54 | double readDouble() { 55 | final doubleBufferUint8 = _doubleBufferUint8 ??= _doubleBuffer.buffer 56 | .asUint8List(); 57 | doubleBufferUint8[0] = readByte(); 58 | doubleBufferUint8[1] = readByte(); 59 | doubleBufferUint8[2] = readByte(); 60 | doubleBufferUint8[3] = readByte(); 61 | doubleBufferUint8[4] = readByte(); 62 | doubleBufferUint8[5] = readByte(); 63 | doubleBufferUint8[6] = readByte(); 64 | doubleBufferUint8[7] = readByte(); 65 | return _doubleBuffer[0]; 66 | } 67 | 68 | Uint8List readBytes(int length) { 69 | final bytes = Uint8List(length); 70 | bytes.setRange(0, bytes.length, _bytes, byteOffset); 71 | byteOffset += bytes.length; 72 | return bytes; 73 | } 74 | 75 | Uint8List readByteList() { 76 | return readBytes(readUInt30()); 77 | } 78 | 79 | Uint8List readOrViewByteList() { 80 | final length = readUInt30(); 81 | final source = _bytes; 82 | final Uint8List view = source.buffer.asUint8List( 83 | source.offsetInBytes + byteOffset, 84 | length, 85 | ); 86 | byteOffset += length; 87 | return view; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /puro/lib/src/workspace/clean.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | import 'package:path/path.dart' as path; 3 | 4 | import '../config.dart'; 5 | import '../logger.dart'; 6 | import '../provider.dart'; 7 | import 'common.dart'; 8 | import 'gitignore.dart'; 9 | import 'intellij.dart'; 10 | import 'vscode.dart'; 11 | 12 | /// Restores IDE settings back to their original. 13 | Future restoreIdeConfigs({ 14 | required Scope scope, 15 | required Directory projectDir, 16 | required ProjectConfig projectConfig, 17 | }) async { 18 | runOptional(scope, 'restoring intellij config', () async { 19 | final ideConfig = await IntelliJConfig.load( 20 | scope: scope, 21 | projectDir: projectDir, 22 | projectConfig: projectConfig, 23 | ); 24 | if (ideConfig.exists) { 25 | await restoreIdeConfig(scope: scope, ideConfig: ideConfig); 26 | } 27 | }); 28 | 29 | runOptional(scope, 'restoring vscode config', () async { 30 | final ideConfig = await VSCodeConfig.load( 31 | scope: scope, 32 | projectDir: projectDir, 33 | projectConfig: projectConfig, 34 | ); 35 | if (ideConfig.exists) { 36 | await restoreIdeConfig(scope: scope, ideConfig: ideConfig); 37 | } 38 | }); 39 | } 40 | 41 | Future restoreIdeConfig({ 42 | required Scope scope, 43 | required IdeConfig ideConfig, 44 | }) async { 45 | final config = PuroConfig.of(scope); 46 | final log = PuroLogger.of(scope); 47 | final envsDir = config.envsDir; 48 | 49 | bool isPuro(Directory? directory) { 50 | if (directory == null) return false; 51 | return path.isWithin(envsDir.path, directory.absolute.path); 52 | } 53 | 54 | // Only restore if the SDK path is an environment, this should be mostly 55 | // harmless either way. 56 | if (isPuro(ideConfig.flutterSdkDir) || isPuro(ideConfig.dartSdkDir)) { 57 | log.v('Restoring ${ideConfig.name}...'); 58 | await ideConfig.restore(scope: scope); 59 | } else { 60 | log.v('${ideConfig.name} already restored'); 61 | } 62 | } 63 | 64 | /// Attempts to restore everything in the workspace back to where it was before 65 | /// using puro, this includes the gitignore and IDE configuration. 66 | Future cleanWorkspace({ 67 | required Scope scope, 68 | Directory? projectDir, 69 | required ProjectConfig projectConfig, 70 | }) async { 71 | final config = PuroConfig.of(scope); 72 | projectDir ??= config.project.ensureParentProjectDir(); 73 | await runOptional(scope, 'restoring gitignore', () { 74 | return updateGitignore(scope: scope, projectDir: projectDir!, ignores: {}); 75 | }); 76 | await runOptional(scope, 'restoring IDE configs', () { 77 | return restoreIdeConfigs( 78 | scope: scope, 79 | projectDir: projectDir!, 80 | projectConfig: projectConfig, 81 | ); 82 | }); 83 | if (config.project.dotfileForWriting.existsSync()) { 84 | config.project.dotfileForWriting.deleteSync(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /puro/lib/src/commands/puro_uninstall.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../command.dart'; 4 | import '../command_result.dart'; 5 | import '../config.dart'; 6 | import '../install/profile.dart'; 7 | import '../terminal.dart'; 8 | import '../version.dart'; 9 | 10 | class PuroUninstallCommand extends PuroCommand { 11 | PuroUninstallCommand() { 12 | argParser.addFlag( 13 | 'force', 14 | help: 15 | 'Ignore the current installation method and attempt to uninstall anyway', 16 | negatable: false, 17 | ); 18 | argParser.addOption( 19 | 'profile', 20 | help: 21 | 'Overrides the profile script puro appends to when updating the PATH', 22 | ); 23 | } 24 | 25 | @override 26 | final name = 'uninstall-puro'; 27 | 28 | @override 29 | final description = 'Uninstalls puro from the system'; 30 | 31 | @override 32 | bool get allowUpdateCheck => false; 33 | 34 | @override 35 | Future run() async { 36 | final puroVersion = await PuroVersion.of(scope); 37 | final config = PuroConfig.of(scope); 38 | final force = argResults!['force'] as bool; 39 | 40 | if (puroVersion.type != PuroInstallationType.distribution && !force) { 41 | throw CommandError( 42 | 'Can only uninstall puro when installed normally, use --force to ignore\n' 43 | '${puroVersion.type.description}', 44 | ); 45 | } 46 | 47 | final prefs = await readGlobalPrefs(scope: scope); 48 | 49 | String? profilePath; 50 | var updatedWindowsRegistry = false; 51 | final homeDir = config.homeDir.path; 52 | if (Platform.isLinux || Platform.isMacOS) { 53 | final profile = await uninstallProfileEnv( 54 | scope: scope, 55 | profileOverride: prefs.hasProfileOverride() 56 | ? prefs.profileOverride 57 | : null, 58 | ); 59 | profilePath = profile?.path.replaceAll(homeDir, '~'); 60 | } else if (Platform.isWindows) { 61 | updatedWindowsRegistry = await tryCleanWindowsPath(scope: scope); 62 | } 63 | 64 | if (profilePath == null && !updatedWindowsRegistry) { 65 | throw CommandError( 66 | 'Could not find Puro in your PATH, is it still installed?', 67 | ); 68 | } 69 | 70 | return BasicMessageResult.list([ 71 | if (profilePath != null) 72 | CommandMessage( 73 | 'Removed Puro from PATH in $profilePath, reopen your terminal for it to take effect', 74 | ), 75 | if (updatedWindowsRegistry) 76 | CommandMessage( 77 | 'Removed Puro from PATH in the Windows registry, reopen your terminal for it to take effect', 78 | ), 79 | CommandMessage.format( 80 | (format) => Platform.isWindows 81 | ? 'To delete environments and settings, delete \'${config.puroRoot.path}\'' 82 | : 'To delete environments and settings, rm -r \'${config.puroRoot.path}\'', 83 | type: CompletionType.info, 84 | ), 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /puro/lib/src/commands/env_use.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../config.dart'; 4 | import '../env/create.dart'; 5 | import '../env/default.dart'; 6 | import '../env/version.dart'; 7 | import '../logger.dart'; 8 | import '../terminal.dart'; 9 | import '../workspace/install.dart'; 10 | import '../workspace/vscode.dart'; 11 | 12 | class EnvUseCommand extends PuroCommand { 13 | EnvUseCommand() { 14 | argParser.addFlag( 15 | 'vscode', 16 | help: 'Enable or disable generation of VSCode configs', 17 | ); 18 | argParser.addFlag( 19 | 'intellij', 20 | help: 21 | 'Enable or disable generation of IntelliJ (or Android Studio) configs', 22 | ); 23 | argParser.addFlag( 24 | 'global', 25 | abbr: 'g', 26 | help: 'Set the global default to the provided environment', 27 | negatable: false, 28 | ); 29 | } 30 | 31 | @override 32 | final name = 'use'; 33 | 34 | @override 35 | final description = 'Selects an environment to use in the current project'; 36 | 37 | @override 38 | String? get argumentUsage => ''; 39 | 40 | @override 41 | Future run() async { 42 | final args = unwrapArguments(atMost: 1); 43 | final config = PuroConfig.of(scope); 44 | final log = PuroLogger.of(scope); 45 | final envName = args.isEmpty ? null : args.first; 46 | if (argResults!['global'] as bool) { 47 | if (envName == null) { 48 | final current = await getDefaultEnvName(scope: scope); 49 | return BasicMessageResult( 50 | 'The current global default environment is `$current`', 51 | type: CompletionType.info, 52 | ); 53 | } 54 | final env = config.getEnv(envName); 55 | if (!env.exists) { 56 | if (isPseudoEnvName(env.name)) { 57 | await createEnvironment( 58 | scope: scope, 59 | envName: env.name, 60 | flutterVersion: await FlutterVersion.query( 61 | scope: scope, 62 | version: env.name, 63 | ), 64 | ); 65 | } else { 66 | log.w('Environment `${env.name}` does not exist'); 67 | } 68 | } 69 | await setDefaultEnvName(scope: scope, envName: env.name); 70 | return BasicMessageResult( 71 | 'Set global default environment to `${env.name}`', 72 | ); 73 | } 74 | var vscodeOverride = argResults!.wasParsed('vscode') 75 | ? argResults!['vscode'] as bool 76 | : null; 77 | if (vscodeOverride == null && await isRunningInVscode(scope: scope)) { 78 | vscodeOverride = true; 79 | } 80 | final environment = await switchEnvironment( 81 | scope: scope, 82 | envName: envName, 83 | vscode: vscodeOverride, 84 | intellij: argResults!.wasParsed('intellij') 85 | ? argResults!['intellij'] as bool 86 | : null, 87 | projectConfig: config.project, 88 | ); 89 | return BasicMessageResult('Switched to environment `${environment.name}`'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /puro/lib/src/commands/engine.dart: -------------------------------------------------------------------------------- 1 | import '../command.dart'; 2 | import '../command_result.dart'; 3 | import '../config.dart'; 4 | import '../engine/build_env.dart'; 5 | import '../engine/prepare.dart'; 6 | 7 | class EngineCommand extends PuroCommand { 8 | EngineCommand() { 9 | addSubcommand(EnginePrepareCommand()); 10 | addSubcommand(EngineBuildEnvCommand()); 11 | } 12 | 13 | @override 14 | final name = 'engine'; 15 | 16 | @override 17 | final description = 'Manages Flutter engine builds'; 18 | } 19 | 20 | class EnginePrepareCommand extends PuroCommand { 21 | EnginePrepareCommand() { 22 | argParser.addOption( 23 | 'fork', 24 | help: 25 | 'The origin to use when cloning the engine, puro will set the upstream automatically.', 26 | valueHelp: 'url', 27 | ); 28 | argParser.addFlag( 29 | 'force', 30 | help: 'Forcefully upgrade the engine, erasing any unstaged changes', 31 | negatable: false, 32 | ); 33 | } 34 | 35 | @override 36 | final name = 'prepare'; 37 | 38 | @override 39 | final description = 'Prepares an environment for building the engine'; 40 | 41 | @override 42 | String? get argumentUsage => ' [ref]'; 43 | 44 | @override 45 | Future run() async { 46 | final force = argResults!['force'] as bool; 47 | final fork = argResults!['fork'] as String?; 48 | final args = unwrapArguments(atLeast: 1, atMost: 2); 49 | final envName = args.first; 50 | final ref = args.length > 1 ? args[1] : null; 51 | 52 | final config = PuroConfig.of(scope); 53 | final env = config.getEnv(envName); 54 | env.ensureExists(); 55 | if (ref != null && ref != env.flutter.engineVersion) { 56 | runner.addMessage( 57 | 'Preparing a different version of the engine than what the framework expects\n' 58 | 'Here be dragons', // rrerr 59 | ); 60 | } 61 | await prepareEngine( 62 | scope: scope, 63 | environment: env, 64 | ref: ref, 65 | forkRemoteUrl: fork, 66 | force: force, 67 | ); 68 | return BasicMessageResult( 69 | 'Engine at `${env.engine.engineSrcDir.path}` ready to build', 70 | ); 71 | } 72 | } 73 | 74 | class EngineBuildEnvCommand extends PuroCommand { 75 | @override 76 | final name = 'build-shell'; 77 | 78 | @override 79 | List get aliases => ['build-env', 'buildenv']; 80 | 81 | @override 82 | final description = 83 | 'Starts a shell with the proper environment variables for building the engine'; 84 | 85 | @override 86 | String? get argumentUsage => ' [...command]'; 87 | 88 | @override 89 | Future run() async { 90 | final config = PuroConfig.of(scope); 91 | 92 | final env = config.getEnv(unwrapArguments(atLeast: 1)[0]); 93 | final command = unwrapArguments(startingAt: 1); 94 | 95 | await prepareEngineSystemDeps(scope: scope); 96 | 97 | final exitCode = await runBuildEnvShell( 98 | scope: scope, 99 | command: command, 100 | environment: env, 101 | ); 102 | 103 | await runner.exitPuro(exitCode); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /puro/lib/src/provider.dart: -------------------------------------------------------------------------------- 1 | abstract class Provider { 2 | factory Provider(T Function(Scope scope) create) = LazyProvider; 3 | 4 | factory Provider.late() { 5 | return Provider((scope) => throw AssertionError('Provider not in scope')); 6 | } 7 | 8 | ProviderNode createNode(Scope scope); 9 | } 10 | 11 | abstract class Scope { 12 | void add(Provider provider, T value); 13 | void replace(Provider provider, T value); 14 | T read(Provider provider); 15 | } 16 | 17 | abstract class ProxyScope implements Scope { 18 | Scope get parent; 19 | 20 | @override 21 | void add(Provider provider, V value) => parent.add(provider, value); 22 | 23 | @override 24 | void replace(Provider provider, V value) => 25 | parent.replace(provider, value); 26 | 27 | @override 28 | V read(Provider provider) => parent.read(provider); 29 | } 30 | 31 | abstract class ProviderNode extends ProxyScope { 32 | ProviderNode(this.parent); 33 | 34 | @override 35 | final Scope parent; 36 | 37 | T get value; 38 | Provider get provider; 39 | 40 | void dispose() {} 41 | } 42 | 43 | class LazyProvider implements Provider { 44 | LazyProvider(this.create); 45 | 46 | final T Function(Scope scope) create; 47 | 48 | @override 49 | ProviderNode createNode(Scope scope) { 50 | return LazyProviderNode(scope, this); 51 | } 52 | } 53 | 54 | class LazyProviderNode extends ProviderNode { 55 | LazyProviderNode(super.parent, this.provider); 56 | 57 | @override 58 | final LazyProvider provider; 59 | 60 | @override 61 | late final value = provider.create(this); 62 | } 63 | 64 | class RootScope extends Scope { 65 | final nodes = , ProviderNode>{}; 66 | final overrides = , Object?>{}; 67 | 68 | @override 69 | void add(Provider provider, T value) { 70 | overrides[provider] = value; 71 | } 72 | 73 | @override 74 | void replace(Provider provider, T value) { 75 | assert(overrides.containsKey(provider) || nodes.containsKey(provider)); 76 | overrides[provider] = value; 77 | final node = nodes[provider]; 78 | if (node != null) { 79 | node.dispose(); 80 | nodes.remove(provider); 81 | } 82 | } 83 | 84 | @override 85 | T read(Provider provider) { 86 | if (overrides.containsKey(provider)) { 87 | return overrides[provider] as T; 88 | } 89 | final node = nodes[provider] ??= provider.createNode(this); 90 | return node.value as T; 91 | } 92 | } 93 | 94 | class OverrideScope extends Scope { 95 | OverrideScope({required this.parent}); 96 | 97 | final Scope parent; 98 | 99 | final overrides = , Object?>{}; 100 | 101 | @override 102 | void add(Provider provider, T value) { 103 | overrides[provider] = value; 104 | } 105 | 106 | @override 107 | void replace(Provider provider, T value) { 108 | if (overrides.containsKey(provider)) { 109 | overrides[provider] = value; 110 | return; 111 | } 112 | parent.replace(provider, value); 113 | } 114 | 115 | @override 116 | T read(Provider provider) { 117 | if (overrides.containsKey(provider)) { 118 | return overrides[provider] as T; 119 | } 120 | return parent.read(provider); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /puro/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Puro is a powerful tool for installing and upgrading [Flutter](https://flutter.dev/) versions, it is essential for any 4 | developers that work on multiple projects or have slower internet. 5 | 6 | With Puro you can: 7 | 8 | * Use different versions of Flutter at the same time 9 | * Download new versions twice as fast with significantly less disk space and internet bandwidth 10 | * Use versions globally or per-project 11 | * Automatically configure IDE settings with a single command 12 | 13 | ## Installation 14 | 15 | Puro is distributed as a precompiled executable (you do not need Dart installed), see the quick installation 16 | instructions at https://puro.dev/ 17 | 18 | ## Quick start 19 | 20 | After installing Puro you can run `puro flutter doctor` to install the latest stable version of Flutter, if you want to 21 | switch to beta you can run `puro use -g beta` and then `puro flutter doctor` again. 22 | 23 | And that's it, you're ready to go! 24 | 25 | Puro uses the concept of "environments" to manage Flutter versions, these can either be tied to a specific version / 26 | release channel, or a named environment that can be upgraded independently. 27 | 28 | Environments can be set globally or per-project, the global environment is set to `stable` by default. 29 | 30 | Cheat sheet: 31 | 32 | ``` 33 | # Create a new environment "foo" with the latest stable release 34 | puro create foo stable 35 | 36 | # Create a new environment "bar" with with Flutter 3.13.6 37 | puro create bar 3.13.6 38 | 39 | # Switch "bar" to a specific Flutter version 40 | puro upgrade bar 3.10.6 41 | 42 | # List available environments 43 | puro ls 44 | 45 | # List available Flutter releases 46 | puro releases 47 | 48 | # Switch the current project to use "foo" 49 | puro use foo 50 | 51 | # Switch the global default to "bar" 52 | puro use -g bar 53 | 54 | # Remove puro configuration from the current project 55 | puro clean 56 | 57 | # Delete the "foo" environment 58 | puro rm foo 59 | 60 | # Run flutter commands in a specific environment 61 | puro -e foo flutter ... 62 | puro -e foo dart ... 63 | puro -e foo pub ... 64 | ``` 65 | 66 | See the full command list at https://puro.dev/reference/manual/ 67 | 68 | ## Performance 69 | 70 | Puro implements a few optimizations that make installing Flutter as fast as possible. 71 | First-time installations are 20% faster while improving subsequent installations by a whopping 50-95%: 72 | 73 | ![](https://puro.dev/assets/install_time_comparison.svg) 74 | 75 | This also translates into much lower network usage: 76 | 77 | ![](https://puro.dev/assets/network_usage_comparison.svg) 78 | 79 | ## How it works 80 | 81 | Puro achieves these performance gains with a few smart optimizations: 82 | 83 | * Parallel git clone and engine download 84 | * Global cache for git history 85 | * Global cache for engine versions 86 | 87 | With other approaches, each Flutter repository is in its own folder, requiring you to download and store the git history, engine, and framework of each version: 88 | 89 | ![](https://puro.dev/assets/storage_without_puro.png) 90 | 91 | Puro implements a technology similar to GitLab's [object deduplication](https://docs.gitlab.com/ee/development/git_object_deduplication.html) to avoid downloading the same git objects over and over again. It also uses symlinks to share the same engine version between multiple installations: 92 | 93 | ![](https://puro.dev/assets/storage_with_puro.png) 94 | -------------------------------------------------------------------------------- /puro/lib/src/commands/repl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_console/dart_console.dart'; 5 | 6 | import '../command.dart'; 7 | import '../command_result.dart'; 8 | import '../env/default.dart'; 9 | import '../eval/context.dart'; 10 | import '../eval/packages.dart'; 11 | import '../eval/worker.dart'; 12 | import '../terminal.dart'; 13 | 14 | class ReplCommand extends PuroCommand { 15 | ReplCommand() { 16 | argParser.addFlag( 17 | 'reset', 18 | abbr: 'r', 19 | help: 'Resets the pubspec file', 20 | negatable: false, 21 | ); 22 | argParser.addMultiOption( 23 | 'extra', 24 | abbr: 'e', 25 | help: 'Extra VM options to pass to the dart executable', 26 | splitCommas: false, 27 | ); 28 | } 29 | 30 | @override 31 | final name = 'repl'; 32 | 33 | @override 34 | final description = 'Interactive REPL for dart code'; 35 | 36 | @override 37 | bool get allowUpdateCheck => false; 38 | 39 | @override 40 | Future run() async { 41 | final extra = argResults!['extra'] as List; 42 | final reset = argResults!['reset'] as bool; 43 | final environment = await getProjectEnvOrDefault(scope: scope); 44 | final context = EvalContext(scope: scope, environment: environment); 45 | context.importCore(); 46 | 47 | try { 48 | await context.pullPackages(packages: const {}, reset: reset); 49 | } on EvalPubError { 50 | CommandMessage( 51 | 'Pass `-r` or `--reset` to use a fresh pubspec file', 52 | type: CompletionType.info, 53 | ).queue(scope); 54 | rethrow; 55 | } on EvalError catch (e) { 56 | throw CommandError('$e'); 57 | } 58 | 59 | final worker = await EvalWorker.spawn( 60 | scope: scope, 61 | context: context, 62 | extra: extra, 63 | ); 64 | 65 | unawaited( 66 | worker.onExit.then((exitCode) async { 67 | if (exitCode != 0) { 68 | CommandMessage( 69 | 'Subprocess exited with code $exitCode', 70 | type: CompletionType.alert, 71 | ).queue(scope); 72 | } 73 | await runner.exitPuro(exitCode); 74 | }), 75 | ); 76 | 77 | final terminal = Terminal.of(scope); 78 | final console = Console.scrolling(recordBlanks: false); 79 | 80 | var didBreak = false; 81 | while (true) { 82 | terminal.flushStatus(); 83 | // TODO(ping): Implement readLine in Terminal 84 | console.write('>>> '); 85 | final line = console.readLine(cancelOnBreak: true); 86 | 87 | if (line == null) { 88 | console.write('^C\n'); 89 | if (!didBreak) { 90 | didBreak = true; 91 | continue; 92 | } 93 | // Break if ctrl-c is pressed twice in a row 94 | break; 95 | } 96 | 97 | try { 98 | final parseResult = context.transform(line); 99 | await worker.reload(parseResult); 100 | final result = await worker.run(); 101 | if (result != null) { 102 | stdout.writeln(result); 103 | } 104 | } catch (e, bt) { 105 | stdout.writeln( 106 | terminal.format.complete( 107 | '$e${e is EvalError ? '' : '\n$bt'}', 108 | type: CompletionType.failure, 109 | ), 110 | ); 111 | } 112 | } 113 | 114 | await worker.dispose(); 115 | await runner.exitPuro(0); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /puro/lib/src/proto/flutter_releases.pbjson.dart: -------------------------------------------------------------------------------- 1 | // This is a generated file - do not edit. 2 | // 3 | // Generated from flutter_releases.proto. 4 | 5 | // @dart = 3.3 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: curly_braces_in_flow_control_structures 10 | // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes 11 | // ignore_for_file: non_constant_identifier_names, prefer_relative_imports 12 | // ignore_for_file: unused_import 13 | 14 | import 'dart:convert' as $convert; 15 | import 'dart:core' as $core; 16 | import 'dart:typed_data' as $typed_data; 17 | 18 | @$core.Deprecated('Use flutterReleaseModelDescriptor instead') 19 | const FlutterReleaseModel$json = { 20 | '1': 'FlutterReleaseModel', 21 | '2': [ 22 | {'1': 'hash', '3': 1, '4': 1, '5': 9, '10': 'hash'}, 23 | {'1': 'channel', '3': 2, '4': 1, '5': 9, '10': 'channel'}, 24 | {'1': 'version', '3': 3, '4': 1, '5': 9, '10': 'version'}, 25 | {'1': 'dart_sdk_version', '3': 4, '4': 1, '5': 9, '10': 'dartSdkVersion'}, 26 | {'1': 'dart_sdk_arch', '3': 5, '4': 1, '5': 9, '10': 'dartSdkArch'}, 27 | {'1': 'release_date', '3': 6, '4': 1, '5': 9, '10': 'releaseDate'}, 28 | {'1': 'archive', '3': 7, '4': 1, '5': 9, '10': 'archive'}, 29 | {'1': 'sha256', '3': 8, '4': 1, '5': 9, '10': 'sha256'}, 30 | ], 31 | }; 32 | 33 | /// Descriptor for `FlutterReleaseModel`. Decode as a `google.protobuf.DescriptorProto`. 34 | final $typed_data.Uint8List flutterReleaseModelDescriptor = $convert.base64Decode( 35 | 'ChNGbHV0dGVyUmVsZWFzZU1vZGVsEhIKBGhhc2gYASABKAlSBGhhc2gSGAoHY2hhbm5lbBgCIA' 36 | 'EoCVIHY2hhbm5lbBIYCgd2ZXJzaW9uGAMgASgJUgd2ZXJzaW9uEigKEGRhcnRfc2RrX3ZlcnNp' 37 | 'b24YBCABKAlSDmRhcnRTZGtWZXJzaW9uEiIKDWRhcnRfc2RrX2FyY2gYBSABKAlSC2RhcnRTZG' 38 | 'tBcmNoEiEKDHJlbGVhc2VfZGF0ZRgGIAEoCVILcmVsZWFzZURhdGUSGAoHYXJjaGl2ZRgHIAEo' 39 | 'CVIHYXJjaGl2ZRIWCgZzaGEyNTYYCCABKAlSBnNoYTI1Ng=='); 40 | 41 | @$core.Deprecated('Use flutterReleasesModelDescriptor instead') 42 | const FlutterReleasesModel$json = { 43 | '1': 'FlutterReleasesModel', 44 | '2': [ 45 | {'1': 'base_url', '3': 1, '4': 1, '5': 9, '10': 'baseUrl'}, 46 | { 47 | '1': 'current_release', 48 | '3': 2, 49 | '4': 3, 50 | '5': 11, 51 | '6': '.FlutterReleasesModel.CurrentReleaseEntry', 52 | '10': 'currentRelease' 53 | }, 54 | { 55 | '1': 'releases', 56 | '3': 3, 57 | '4': 3, 58 | '5': 11, 59 | '6': '.FlutterReleaseModel', 60 | '10': 'releases' 61 | }, 62 | ], 63 | '3': [FlutterReleasesModel_CurrentReleaseEntry$json], 64 | }; 65 | 66 | @$core.Deprecated('Use flutterReleasesModelDescriptor instead') 67 | const FlutterReleasesModel_CurrentReleaseEntry$json = { 68 | '1': 'CurrentReleaseEntry', 69 | '2': [ 70 | {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, 71 | {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, 72 | ], 73 | '7': {'7': true}, 74 | }; 75 | 76 | /// Descriptor for `FlutterReleasesModel`. Decode as a `google.protobuf.DescriptorProto`. 77 | final $typed_data.Uint8List flutterReleasesModelDescriptor = $convert.base64Decode( 78 | 'ChRGbHV0dGVyUmVsZWFzZXNNb2RlbBIZCghiYXNlX3VybBgBIAEoCVIHYmFzZVVybBJSCg9jdX' 79 | 'JyZW50X3JlbGVhc2UYAiADKAsyKS5GbHV0dGVyUmVsZWFzZXNNb2RlbC5DdXJyZW50UmVsZWFz' 80 | 'ZUVudHJ5Ug5jdXJyZW50UmVsZWFzZRIwCghyZWxlYXNlcxgDIAMoCzIULkZsdXR0ZXJSZWxlYX' 81 | 'NlTW9kZWxSCHJlbGVhc2VzGkEKE0N1cnJlbnRSZWxlYXNlRW50cnkSEAoDa2V5GAEgASgJUgNr' 82 | 'ZXkSFAoFdmFsdWUYAiABKAlSBXZhbHVlOgI4AQ=='); 83 | -------------------------------------------------------------------------------- /puro/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | errors: 3 | missing_required_param: warning 4 | missing_return: warning 5 | exclude: 6 | - lib/src/proto/*.dart 7 | language: 8 | strict-casts: true 9 | strict-raw-types: true 10 | 11 | linter: 12 | rules: 13 | - unnecessary_string_escapes 14 | - always_declare_return_types 15 | - annotate_overrides 16 | - avoid_bool_literals_in_conditional_expressions 17 | - avoid_classes_with_only_static_members 18 | - avoid_empty_else 19 | - avoid_equals_and_hash_code_on_mutable_classes 20 | - avoid_field_initializers_in_const_classes 21 | - avoid_function_literals_in_foreach_calls 22 | - avoid_init_to_null 23 | - avoid_null_checks_in_equality_operators 24 | - prefer_relative_imports 25 | - avoid_renaming_method_parameters 26 | - avoid_return_types_on_setters 27 | - avoid_returning_null_for_void 28 | - avoid_single_cascade_in_expression_statements 29 | - avoid_slow_async_io 30 | - avoid_types_as_parameter_names 31 | - avoid_unused_constructor_parameters 32 | - avoid_void_async 33 | - avoid_print 34 | - await_only_futures 35 | - camel_case_extensions 36 | - camel_case_types 37 | - cancel_subscriptions 38 | - control_flow_in_finally 39 | - directives_ordering 40 | - empty_catches 41 | - empty_constructor_bodies 42 | - empty_statements 43 | - flutter_style_todos 44 | - hash_and_equals 45 | - implementation_imports 46 | - collection_methods_unrelated_type 47 | - library_names 48 | - library_prefixes 49 | - no_adjacent_strings_in_list 50 | - no_duplicate_case_values 51 | - non_constant_identifier_names 52 | - overridden_fields 53 | - package_names 54 | - package_prefixed_library_names 55 | - prefer_adjacent_string_concatenation 56 | - prefer_asserts_in_initializer_lists 57 | - prefer_collection_literals 58 | - prefer_conditional_assignment 59 | - prefer_const_constructors 60 | - prefer_const_constructors_in_immutables 61 | - prefer_const_declarations 62 | - prefer_const_literals_to_create_immutables 63 | - prefer_contains 64 | - prefer_final_fields 65 | - prefer_final_in_for_each 66 | - prefer_final_locals 67 | - prefer_for_elements_to_map_fromIterable 68 | - prefer_foreach 69 | - prefer_generic_function_type_aliases 70 | - prefer_if_elements_to_conditional_expressions 71 | - prefer_if_null_operators 72 | - prefer_initializing_formals 73 | - prefer_inlined_adds 74 | - prefer_is_empty 75 | - prefer_is_not_empty 76 | - prefer_is_not_operator 77 | - prefer_iterable_whereType 78 | - prefer_single_quotes 79 | - prefer_spread_collections 80 | - prefer_typing_uninitialized_variables 81 | - prefer_void_to_null 82 | - recursive_getters 83 | - slash_for_doc_comments 84 | - sort_pub_dependencies 85 | - sort_unnamed_constructors_first 86 | - test_types_in_equals 87 | - throw_in_finally 88 | - type_init_formals 89 | - unnecessary_brace_in_string_interps 90 | - unnecessary_const 91 | - unnecessary_getters_setters 92 | - unnecessary_new 93 | - unnecessary_null_aware_assignments 94 | - unnecessary_null_in_if_null_operators 95 | - unnecessary_overrides 96 | - unnecessary_parenthesis 97 | - unnecessary_statements 98 | - unnecessary_this 99 | - unrelated_type_equality_checks 100 | - use_full_hex_values_for_flutter_colors 101 | - use_key_in_widget_constructors 102 | - use_rethrow_when_possible 103 | - valid_regexps 104 | - void_checks 105 | - unawaited_futures 106 | -------------------------------------------------------------------------------- /puro/lib/src/env/default.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../command_result.dart'; 4 | import '../config.dart'; 5 | import '../file_lock.dart'; 6 | import '../provider.dart'; 7 | import 'create.dart'; 8 | import 'releases.dart'; 9 | import 'version.dart'; 10 | 11 | bool isPseudoEnvName(String name) { 12 | return pseudoEnvironmentNames.contains(name) || isValidVersion(name); 13 | } 14 | 15 | Future _getPseudoEnvironment({ 16 | required Scope scope, 17 | required String envName, 18 | }) async { 19 | final config = PuroConfig.of(scope); 20 | final environment = config.getEnv(envName); 21 | if (!environment.exists) { 22 | await createEnvironment( 23 | scope: scope, 24 | envName: environment.name, 25 | flutterVersion: await FlutterVersion.query( 26 | scope: scope, 27 | version: environment.name, 28 | ), 29 | ); 30 | } 31 | return environment; 32 | } 33 | 34 | Future getProjectEnvOrDefault({ 35 | required Scope scope, 36 | String? envName, 37 | }) async { 38 | final config = PuroConfig.of(scope); 39 | if (envName != null) { 40 | final environment = config.getEnv(envName); 41 | if (!environment.exists) { 42 | if (isPseudoEnvName(environment.name)) { 43 | return _getPseudoEnvironment(scope: scope, envName: envName); 44 | } 45 | environment.ensureExists(); 46 | } 47 | return environment; 48 | } 49 | var env = config.tryGetProjectEnv(); 50 | if (env == null) { 51 | final override = config.environmentOverride; 52 | if (override != null) { 53 | if (isPseudoEnvName(override)) { 54 | return _getPseudoEnvironment(scope: scope, envName: override); 55 | } 56 | throw CommandError( 57 | 'Selected environment `${config.environmentOverride}` does not exist', 58 | ); 59 | } 60 | final envName = await getDefaultEnvName(scope: scope); 61 | if (isPseudoEnvName(envName)) { 62 | return _getPseudoEnvironment(scope: scope, envName: envName); 63 | } 64 | env = config.getEnv(envName); 65 | if (!env.exists) { 66 | throw CommandError( 67 | 'No environment selected and default environment `$envName` does not exist', 68 | ); 69 | } 70 | } 71 | return env; 72 | } 73 | 74 | Future getDefaultEnvName({required Scope scope}) async { 75 | final prefs = await readGlobalPrefs(scope: scope); 76 | return prefs.hasDefaultEnvironment() ? prefs.defaultEnvironment : 'stable'; 77 | } 78 | 79 | Future setDefaultEnvName({ 80 | required Scope scope, 81 | required String envName, 82 | }) async { 83 | ensureValidEnvName(envName); 84 | await updateGlobalPrefs( 85 | scope: scope, 86 | fn: (prefs) { 87 | prefs.defaultEnvironment = envName; 88 | }, 89 | ); 90 | await updateDefaultEnvSymlink(scope: scope, name: envName); 91 | } 92 | 93 | Future updateDefaultEnvSymlink({ 94 | required Scope scope, 95 | String? name, 96 | }) async { 97 | final config = PuroConfig.of(scope); 98 | name ??= await getDefaultEnvName(scope: scope); 99 | final environment = config.getEnv(name); 100 | final link = config.defaultEnvLink; 101 | 102 | if (environment.exists) { 103 | final path = environment.envDir.path; 104 | if (!FileSystemEntity.isLinkSync(link.path)) { 105 | if (link.existsSync()) { 106 | link.deleteSync(); 107 | } 108 | link.parent.createSync(recursive: true); 109 | await createLink(scope: scope, link: link, path: path); 110 | } else if (link.targetSync() != path) { 111 | link.updateSync(path); 112 | } 113 | } else if (FileSystemEntity.isLinkSync(link.path)) { 114 | link.deleteSync(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /puro/lib/src/workspace/gitignore.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:file/file.dart'; 4 | 5 | import '../config.dart'; 6 | import '../extensions.dart'; 7 | import '../logger.dart'; 8 | import '../provider.dart'; 9 | 10 | const gitIgnoredFilesForWorkspace = {ProjectConfig.dotfileName}; 11 | const gitConfigComment = '# Managed by puro'; 12 | 13 | Future updateConfigLines({ 14 | required Scope scope, 15 | required File file, 16 | required Set lines, 17 | }) async { 18 | file.createSync(recursive: true); 19 | final log = PuroLogger.of(scope); 20 | final result = const LineSplitter().convert(file.readAsStringSync()); 21 | final existingLines = {}; 22 | for (var i = 0; i < result.length;) { 23 | if (result[i] == gitConfigComment && i + 1 < result.length) { 24 | existingLines.add(result[i + 1]); 25 | result.removeAt(i); 26 | result.removeAt(i); 27 | } else { 28 | i++; 29 | } 30 | } 31 | while (result.isNotEmpty && result.last.isEmpty) result.removeLast(); 32 | if (!existingLines.containsAll(lines) || 33 | existingLines.length != lines.length) { 34 | log.v('Updating config at ${file.path}'); 35 | file.writeAsStringSync( 36 | [ 37 | ...result, 38 | '', 39 | for (final name in lines) ...[gitConfigComment, name], 40 | ].join('\n'), 41 | ); 42 | for (final line in lines) { 43 | if (!existingLines.contains(line)) { 44 | log.v('Added "$line"'); 45 | } 46 | } 47 | for (final line in existingLines) { 48 | if (!lines.contains(line)) { 49 | log.v('Removed "$line"'); 50 | } 51 | } 52 | } 53 | } 54 | 55 | Directory? findGitDir(Directory projectDir) { 56 | final fileSystem = projectDir.fileSystem; 57 | final gitTree = findProjectDir(projectDir, '.git'); 58 | if (gitTree == null) return null; 59 | final gitDir = gitTree.childDirectory('.git'); 60 | if (fileSystem.statSync(gitDir.path).type == FileSystemEntityType.file) { 61 | final match = RegExp( 62 | r'gitdir: (.+)', 63 | ).firstMatch(fileSystem.file(gitDir.path).readAsStringSync().trim()); 64 | if (match != null) { 65 | final gitTree2 = findProjectDir( 66 | fileSystem.directory(match.group(1)!), 67 | '.git', 68 | ); 69 | if (gitTree2 != null) { 70 | return gitTree2.childDirectory('.git').resolve(); 71 | } 72 | } 73 | return null; 74 | } else { 75 | return gitDir.resolve(); 76 | } 77 | } 78 | 79 | /// Adds the dotfile to .git/info/exclude which is a handy way to ignore it 80 | /// without touching the working tree or global git configuration. 81 | Future updateGitignore({ 82 | required Scope scope, 83 | required Directory projectDir, 84 | required Set ignores, 85 | }) async { 86 | final gitDir = findGitDir(projectDir); 87 | if (gitDir == null) return; 88 | final log = PuroLogger.of(scope); 89 | log.v('Updating ${gitDir.path}/info/exclude'); 90 | await updateConfigLines( 91 | scope: scope, 92 | file: gitDir.childDirectory('info').childFile('exclude'), 93 | lines: ignores, 94 | ); 95 | } 96 | 97 | Future updateGitAttributes({ 98 | required Scope scope, 99 | required Directory projectDir, 100 | required Map attributes, 101 | }) async { 102 | final gitTree = findProjectDir(projectDir, '.git'); 103 | if (gitTree == null) return; 104 | await updateConfigLines( 105 | scope: scope, 106 | file: gitTree 107 | .childDirectory('.git') 108 | .childDirectory('info') 109 | .childFile('attributes'), 110 | lines: { 111 | for (final entry in attributes.entries) '${entry.key} ${entry.value}', 112 | }, 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /puro/lib/src/commands/puro_upgrade.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pub_semver/pub_semver.dart'; 4 | 5 | import '../command.dart'; 6 | import '../command_result.dart'; 7 | import '../config.dart'; 8 | import '../http.dart'; 9 | import '../install/upgrade.dart'; 10 | import '../process.dart'; 11 | import '../terminal.dart'; 12 | import '../version.dart'; 13 | 14 | class PuroUpgradeCommand extends PuroCommand { 15 | PuroUpgradeCommand() { 16 | argParser.addFlag( 17 | 'force', 18 | hide: true, 19 | help: 20 | 'Installs a new puro executable even if it wont replace an existing one', 21 | negatable: false, 22 | ); 23 | argParser.addFlag( 24 | 'path', 25 | help: 'Whether or not to update the PATH automatically', 26 | ); 27 | } 28 | 29 | @override 30 | final name = 'upgrade-puro'; 31 | 32 | @override 33 | List get aliases => ['update-puro']; 34 | 35 | @override 36 | String? get argumentUsage => '[version]'; 37 | 38 | @override 39 | final description = 'Upgrades the puro tool to a new version'; 40 | 41 | @override 42 | bool get allowUpdateCheck => false; 43 | 44 | @override 45 | Future run() async { 46 | final force = argResults!['force'] as bool; 47 | final http = scope.read(clientProvider); 48 | final config = PuroConfig.of(scope); 49 | final puroVersion = await PuroVersion.of(scope); 50 | final currentVersion = puroVersion.semver; 51 | var targetVersionString = unwrapSingleOptionalArgument(); 52 | 53 | if (puroVersion.type == PuroInstallationType.pub) { 54 | final result = await runProcess(scope, Platform.resolvedExecutable, [ 55 | 'pub', 56 | 'global', 57 | 'activate', 58 | 'puro', 59 | ]); 60 | if (result.exitCode == 0) { 61 | final stdout = result.stdout as String; 62 | if (stdout.contains('already activated at newest available version')) { 63 | return BasicMessageResult('Puro is up to date with $currentVersion'); 64 | } else { 65 | return BasicMessageResult('Upgraded puro to latest pub version'); 66 | } 67 | } else { 68 | return BasicMessageResult( 69 | '`dart pub global activate puro` failed with exit code ${result.exitCode}\n${result.stderr}' 70 | .trim(), 71 | success: false, 72 | ); 73 | } 74 | } else if (puroVersion.type != PuroInstallationType.distribution && 75 | !force) { 76 | return BasicMessageResult( 77 | "Can't upgrade: ${puroVersion.type.description}", 78 | success: false, 79 | ); 80 | } 81 | 82 | if (targetVersionString == 'master') { 83 | final exitCode = await upgradePuro( 84 | scope: scope, 85 | targetVersion: 'master', 86 | path: argResults!.wasParsed('path') 87 | ? argResults!['path'] as bool 88 | : null, 89 | ); 90 | await runner.exitPuro(exitCode); 91 | } 92 | 93 | final Version targetVersion; 94 | if (targetVersionString == null) { 95 | final latestVersionResponse = await http.get( 96 | config.puroBuildsUrl.append(path: 'latest'), 97 | ); 98 | HttpException.ensureSuccess(latestVersionResponse); 99 | targetVersionString = latestVersionResponse.body.trim(); 100 | targetVersion = Version.parse(targetVersionString); 101 | if (currentVersion == targetVersion && !force) { 102 | return BasicMessageResult('Puro is up to date with $targetVersion'); 103 | } else if (currentVersion > targetVersion && !force) { 104 | return BasicMessageResult( 105 | 'Puro is a newer version $currentVersion than the available $targetVersion', 106 | type: CompletionType.indeterminate, 107 | ); 108 | } 109 | } else { 110 | targetVersion = Version.parse(targetVersionString); 111 | if (currentVersion == targetVersion && !force) { 112 | return BasicMessageResult('Puro is the desired version $targetVersion'); 113 | } 114 | } 115 | 116 | final exitCode = await upgradePuro( 117 | scope: scope, 118 | targetVersion: '$targetVersion', 119 | path: argResults!.wasParsed('path') ? argResults!['path'] as bool : null, 120 | ); 121 | 122 | await runner.exitPuro(exitCode); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /puro/lib/src/env/gc.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | 3 | import '../config.dart'; 4 | import '../git.dart'; 5 | import '../logger.dart'; 6 | import '../provider.dart'; 7 | 8 | Future collectGarbage({ 9 | required Scope scope, 10 | int maxUnusedCaches = 5, 11 | int maxUnusedFlutterTools = 5, 12 | }) async { 13 | final log = PuroLogger.of(scope); 14 | final config = PuroConfig.of(scope); 15 | final git = GitClient.of(scope); 16 | final sharedCacheDirs = config.sharedCachesDir.existsSync() 17 | ? config.sharedCachesDir.listSync() 18 | : []; 19 | final flutterToolDirs = config.sharedFlutterToolsDir.existsSync() 20 | ? config.sharedFlutterToolsDir.listSync() 21 | : []; 22 | if (sharedCacheDirs.length < maxUnusedCaches && 23 | flutterToolDirs.length < maxUnusedFlutterTools) { 24 | // Don't bother cleaning up if there are less than maxUnusedCaches 25 | return 0; 26 | } 27 | final usedCaches = {}; 28 | final usedCommits = {}; 29 | for (final dir in config.envsDir.listSync()) { 30 | if (dir is! Directory || !isValidEnvName(dir.basename)) { 31 | continue; 32 | } 33 | final environment = config.getEnv(dir.basename); 34 | final engineVersion = environment.flutter.engineVersion; 35 | if (engineVersion != null) { 36 | usedCaches.add(engineVersion); 37 | } 38 | 39 | final commit = await git.tryGetCurrentCommitHash( 40 | repository: environment.flutterDir, 41 | ); 42 | if (commit != null) { 43 | usedCommits.add(commit); 44 | } 45 | } 46 | 47 | final unusedCaches = {}; 48 | for (final dir in sharedCacheDirs) { 49 | if (dir is! Directory || 50 | !isValidCommitHash(dir.basename) || 51 | usedCaches.contains(dir.basename)) 52 | continue; 53 | final config = FlutterCacheConfig(dir); 54 | final versionFile = config.engineVersionFile; 55 | if (versionFile.existsSync()) { 56 | unusedCaches[dir] = config.engineVersionFile.lastAccessedSync(); 57 | } else { 58 | // Perhaps a delete was incomplete? The cache is invalid without an 59 | // engine version file regardless. 60 | unusedCaches[dir] = DateTime.fromMillisecondsSinceEpoch(0); 61 | } 62 | } 63 | 64 | final unusedFlutterTools = {}; 65 | for (final dir in flutterToolDirs) { 66 | if (dir is! Directory || 67 | !isValidCommitHash(dir.basename) || 68 | usedCommits.contains(dir.basename)) 69 | continue; 70 | final snapshotFile = dir.childFile('flutter_tool.snapshot'); 71 | if (snapshotFile.existsSync()) { 72 | unusedFlutterTools[dir] = snapshotFile.lastAccessedSync(); 73 | } else { 74 | unusedCaches[dir] = DateTime.fromMillisecondsSinceEpoch(0); 75 | } 76 | } 77 | 78 | Future deleteRecursive(FileSystemEntity entity) async { 79 | final stat = entity.statSync(); 80 | if (stat.type == FileSystemEntityType.notFound) { 81 | return 0; 82 | } 83 | var size = stat.size; 84 | if (entity is Directory && stat.type == FileSystemEntityType.directory) { 85 | await for (final child in entity.list()) { 86 | size += await deleteRecursive(child); 87 | } 88 | } 89 | // This shouldn't need to be recursive but I saw a "Directory not empty" 90 | // error in the wild. 91 | entity.deleteSync(recursive: true); 92 | return size; 93 | } 94 | 95 | var reclaimed = 0; 96 | 97 | // In theory this should be the access times in ascending order but I never 98 | // tested it (famous last words) 99 | var entries = unusedCaches.entries.toList() 100 | ..sort((a, b) => a.value.compareTo(b.value)); 101 | for (var i = 0; i < entries.length - maxUnusedCaches; i++) { 102 | final dir = entries[i].key; 103 | log.v('Deleting ${dir.path}'); 104 | reclaimed += await deleteRecursive(dir); 105 | } 106 | 107 | // Same thing for snapshot files 108 | entries = unusedFlutterTools.entries.toList() 109 | ..sort((a, b) => a.value.compareTo(b.value)); 110 | for (var i = 0; i < entries.length - maxUnusedFlutterTools; i++) { 111 | final dir = entries[i].key; 112 | log.v('Deleting ${dir.path}'); 113 | reclaimed += await deleteRecursive(dir); 114 | } 115 | 116 | for (final zipFile in sharedCacheDirs) { 117 | if (zipFile is! File || !zipFile.basename.endsWith('.zip')) continue; 118 | reclaimed += await deleteRecursive(zipFile); 119 | } 120 | 121 | return reclaimed; 122 | } 123 | -------------------------------------------------------------------------------- /puro/lib/src/eval/packages.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:pub_semver/pub_semver.dart'; 4 | import 'package:typed_data/typed_buffers.dart'; 5 | import 'package:yaml/yaml.dart'; 6 | import 'package:yaml_edit/yaml_edit.dart'; 7 | 8 | import '../command_result.dart'; 9 | import '../config.dart'; 10 | import '../env/command.dart'; 11 | import '../file_lock.dart'; 12 | import '../logger.dart'; 13 | import '../provider.dart'; 14 | 15 | class EvalPubError extends CommandError { 16 | EvalPubError(String message) : super(message); 17 | } 18 | 19 | Future updateBootstrapPackages({ 20 | required Scope scope, 21 | required EnvConfig environment, 22 | required String sdkVersion, 23 | required Map packages, 24 | bool reset = false, 25 | }) async { 26 | final log = PuroLogger.of(scope); 27 | final bootstrapDir = environment.evalBootstrapDir; 28 | final pubspecLockFile = bootstrapDir.childFile('pubspec.lock'); 29 | final pubspecYamlFile = bootstrapDir.childFile('pubspec.yaml'); 30 | final updateLockFile = bootstrapDir.childFile('update.lock'); 31 | bootstrapDir.createSync(recursive: true); 32 | 33 | return await lockFile(scope, updateLockFile, (handle) async { 34 | if (!reset) { 35 | var satisfied = true; 36 | log.d('pubspecLockFile exists'); 37 | final existingPackages = {}; 38 | if (pubspecLockFile.existsSync()) { 39 | final yamlData = 40 | loadYaml( 41 | pubspecLockFile.existsSync() 42 | ? pubspecLockFile.readAsStringSync() 43 | : '{}', 44 | ) 45 | as YamlMap; 46 | for (final package in (yamlData['packages'] as YamlMap).entries) { 47 | final name = package.key as String; 48 | final version = Version.parse(package.value['version'] as String); 49 | existingPackages[name] = version; 50 | } 51 | } 52 | log.d('existingPackages: $existingPackages'); 53 | for (final entry in packages.entries) { 54 | final hasPackage = existingPackages.containsKey(entry.key); 55 | if ((hasPackage == (entry.value == VersionConstraint.empty)) || 56 | (hasPackage && 57 | !(entry.value ?? VersionConstraint.any).allows( 58 | existingPackages[entry.key]!, 59 | ))) { 60 | log.d( 61 | 'not satisfied: ${entry.key} must be ${entry.value} ' 62 | 'but is ${existingPackages[entry.key]}', 63 | ); 64 | satisfied = false; 65 | break; 66 | } 67 | } 68 | if (satisfied) return false; 69 | } 70 | 71 | final yaml = YamlEditor( 72 | !reset && pubspecYamlFile.existsSync() 73 | ? await readAtomic(scope: scope, file: pubspecYamlFile) 74 | : '{}', 75 | ); 76 | 77 | bool exists(Iterable path) => 78 | yaml.parseAt(path, orElse: () => YamlScalar.wrap(null)).value != null; 79 | 80 | yaml.update(['name'], 'bootstrap'); 81 | yaml.update(['version'], '0.0.1'); 82 | yaml.update(['environment'], {'sdk': sdkVersion}); 83 | 84 | if (!exists(['dependencies'])) { 85 | yaml.update(['dependencies'], YamlMap()); 86 | } 87 | 88 | for (final entry in packages.entries) { 89 | final key = ['dependencies', entry.key]; 90 | if (entry.value == null) { 91 | if (!exists(key)) { 92 | yaml.update(key, 'any'); 93 | } 94 | } else if (entry.value == VersionConstraint.empty) { 95 | yaml.remove(key); 96 | } else { 97 | yaml.update(key, '${entry.value}'); 98 | } 99 | } 100 | 101 | final original = pubspecYamlFile.existsSync() 102 | ? pubspecYamlFile.readAsStringSync() 103 | : null; 104 | pubspecYamlFile.writeAsStringSync('$yaml'); 105 | 106 | final stdoutBuffer = Uint8Buffer(); 107 | final stderrBuffer = Uint8Buffer(); 108 | final result = await runDartCommand( 109 | scope: scope, 110 | environment: environment, 111 | args: ['pub', 'get'], 112 | workingDirectory: bootstrapDir.path, 113 | onStdout: stdoutBuffer.addAll, 114 | onStderr: stderrBuffer.addAll, 115 | ); 116 | if (result != 0 || !pubspecLockFile.existsSync()) { 117 | if (original != null) { 118 | pubspecYamlFile.writeAsStringSync(original); 119 | } else { 120 | pubspecYamlFile.deleteSync(); 121 | } 122 | throw EvalPubError( 123 | 'pub get failed in `${bootstrapDir.path}`\n' 124 | '${utf8.decode(stdoutBuffer)}${utf8.decode(stderrBuffer)}', 125 | ); 126 | } 127 | 128 | return true; 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /puro/lib/src/commands/eval.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:pub_semver/pub_semver.dart'; 5 | 6 | import '../command.dart'; 7 | import '../command_result.dart'; 8 | import '../env/default.dart'; 9 | import '../eval/context.dart'; 10 | import '../eval/packages.dart'; 11 | import '../eval/worker.dart'; 12 | import '../logger.dart'; 13 | import '../terminal.dart'; 14 | 15 | class EvalCommand extends PuroCommand { 16 | EvalCommand() { 17 | argParser.addFlag( 18 | 'reset', 19 | abbr: 'r', 20 | help: 'Resets the pubspec file', 21 | negatable: false, 22 | ); 23 | argParser.addMultiOption( 24 | 'import', 25 | aliases: ['imports'], 26 | abbr: 'i', 27 | help: 28 | 'A package to import, this option accepts a shortened package ' 29 | 'URI followed by one or more optional modifiers\n\n' 30 | 'Shortened names expand as follows:\n' 31 | " foo => import 'package:foo/foo.dart'\n" 32 | " foo/bar => import 'package:foo/bar.dart'\n\n" 33 | 'The `=` modifier adds `as` to the import:\n' 34 | " foo= => import 'package:foo/foo.dart' as foo\n" 35 | " foo=bar => import 'package:foo/foo.dart' as bar\n\n" 36 | " foo/bar= => import 'package:foo/bar.dart' as foo\n\n" 37 | 'The `+` and `-` modifier add `show` and `hide` to the import:\n' 38 | " foo+x => import 'package:foo/foo.dart' show x\n" 39 | " foo+x+y => import 'package:foo/foo.dart' show x, y\n" 40 | " foo-x => import 'package:foo/foo.dart' hide x\n" 41 | " foo-x-y => import 'package:foo/foo.dart' hide x, y\n\n" 42 | 'Imports for packages also implicitly add a package dependency', 43 | ); 44 | argParser.addMultiOption( 45 | 'package', 46 | aliases: ['packages'], 47 | abbr: 'p', 48 | help: 49 | 'A package to depend on, this option accepts the package name ' 50 | 'optionally followed by a version constraint:\n' 51 | ' name[`=`][constraint]\n' 52 | 'The package is removed from the pubspec if constraint is "none"', 53 | ); 54 | argParser.addFlag( 55 | 'no-core', 56 | abbr: 'c', 57 | help: 'Whether to disable automatic imports of core libraries', 58 | negatable: false, 59 | ); 60 | argParser.addMultiOption( 61 | 'extra', 62 | abbr: 'e', 63 | help: 'Extra VM options to pass to the dart executable', 64 | splitCommas: false, 65 | ); 66 | } 67 | 68 | @override 69 | final name = 'eval'; 70 | 71 | @override 72 | final description = 'Evaluates ephemeral Dart code'; 73 | 74 | @override 75 | String? get argumentUsage => '[code]'; 76 | 77 | @override 78 | bool get allowUpdateCheck => false; 79 | 80 | @override 81 | Future run() async { 82 | final log = PuroLogger.of(scope); 83 | final environment = await getProjectEnvOrDefault(scope: scope); 84 | 85 | final noCoreImports = argResults!['no-core'] as bool; 86 | final reset = argResults!['reset'] as bool; 87 | final imports = (argResults!['import'] as List) 88 | .map(EvalImport.parse) 89 | .toList(); 90 | final packages = argResults!['package'] as List; 91 | final extra = argResults!['extra'] as List; 92 | var code = argResults!.rest.join(' '); 93 | if (code.isEmpty) { 94 | code = await utf8.decodeStream(stdin); 95 | } 96 | 97 | final context = EvalContext(scope: scope, environment: environment); 98 | if (!noCoreImports) context.importCore(); 99 | context.imports.addAll(imports); 100 | final packageVersions = { 101 | for (final import in imports) 102 | if (import.uri.scheme == 'package') import.uri.pathSegments.first: null, 103 | }; 104 | packageVersions.addEntries(packages.map(parseEvalPackage)); 105 | log.d('packageVersions: $packageVersions'); 106 | 107 | try { 108 | await context.pullPackages(packages: packageVersions, reset: reset); 109 | 110 | final worker = await EvalWorker.spawn( 111 | scope: scope, 112 | context: context, 113 | code: code, 114 | extra: extra, 115 | ); 116 | 117 | final result = await worker.run(); 118 | if (result != null) { 119 | stdout.writeln(result); 120 | } 121 | await worker.dispose(); 122 | await runner.exitPuro(0); 123 | } on EvalPubError { 124 | CommandMessage( 125 | 'Pass `-r` or `--reset` to use a fresh pubspec file', 126 | type: CompletionType.info, 127 | ).queue(scope); 128 | rethrow; 129 | } on EvalError catch (e) { 130 | throw CommandError('$e'); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /website/docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | /* Make tabs smaller and add indicator */ 2 | .md-footer .md-social { 3 | padding: 0; 4 | } 5 | .md-tabs .md-tabs__link { 6 | margin: 0; 7 | } 8 | .md-tabs .md-tabs__item { 9 | height: inherit; 10 | padding: 0; 11 | display: flex; 12 | align-content: stretch; 13 | } 14 | 15 | .md-tabs .md-tabs__item a { 16 | vertical-align: middle; 17 | padding: 0 8px; 18 | margin: 0 8px; 19 | } 20 | .md-tabs .md-tabs__link { 21 | box-sizing: border-box; 22 | border-bottom: 2px solid rgba(0, 0, 0, 0); 23 | } 24 | .md-tabs .md-tabs__link--active { 25 | border-bottom: 2px solid; 26 | } 27 | .md-tabs .md-tabs__list { 28 | height: 48px; 29 | display: flex; 30 | flex-direction: row; 31 | } 32 | 33 | 34 | /* Make content a little wider */ 35 | .md-grid { 36 | max-width: 1300px; 37 | } 38 | 39 | /* Disable breakpoint text scaling */ 40 | html { 41 | font-size: 125%; 42 | } 43 | 44 | /* Bigger logo */ 45 | .md-header__button.md-logo :-webkit-any(img,svg) { 46 | height: 32px; 47 | } 48 | .md-header__button.md-logo { 49 | padding: 0; 50 | } 51 | 52 | /* Make sidebar invisible instead of collapsed when hidden */ 53 | .md-main .md-sidebar--primary[hidden] { 54 | display: initial; 55 | visibility: hidden; 56 | } 57 | 58 | @media screen and (max-width: 76.1875em) { 59 | .md-main .md-sidebar--primary[hidden] { 60 | visibility: visible; 61 | } 62 | } 63 | 64 | [data-md-color-primary="white"] { 65 | --md-button-hover-bg-color: hsla(0, 0, 0, 0.1); 66 | } 67 | 68 | [data-md-color-primary="slate"] { 69 | --md-button-hover-bg-color: hsla(0, 0, 0, 0.1); 70 | } 71 | 72 | /* Better looking buttons */ 73 | .md-typeset .md-button { 74 | border-radius: 4px; 75 | margin: 4px; 76 | color: var(--md-typeset-color); 77 | border-color: var(--md-default-fg-color--lightest); 78 | } 79 | 80 | .md-typeset .md-button:is(:focus,:hover):not(.md-button--primary) { 81 | background-color: var(--md-default-fg-color--lightest); 82 | border-color: var(--md-default-fg-color--lightest); 83 | } 84 | 85 | .md-typeset .md-button.md-button--primary { 86 | color: var(--md-primary-bg-color); 87 | border-color: var(--md-primary-fg-color); 88 | } 89 | 90 | .md-typeset .md-button:is(:focus,:hover) { 91 | background-color: var(--md-primary-fg-color); 92 | border-color: var(--md-default-bg-color--light); 93 | } 94 | 95 | /* Better looking tabs */ 96 | .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child, 97 | .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2), 98 | .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3), 99 | .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4), 100 | .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5), 101 | .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6), 102 | .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7), 103 | .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8), 104 | .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9), 105 | .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10), 106 | .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11), 107 | .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12), 108 | .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13), 109 | .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14), 110 | .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15), 111 | .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16), 112 | .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17), 113 | .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18), 114 | .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19), 115 | .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) { 116 | color: var(--md-typeset-a-color); 117 | } 118 | .md-typeset .tabbed-labels>label:hover { 119 | color: var(--md-typeset-color--light); 120 | } 121 | .js .md-typeset .tabbed-labels:before { 122 | background: var(--md-typeset-a-color); 123 | } 124 | 125 | /* 🌠 */ 126 | .star-list li { 127 | text-indent: 4px; 128 | list-style-image: url(/assets/star.svg); 129 | } 130 | .star-list li::marker { 131 | font-size: 2rem; 132 | margin: 0; 133 | line-height: 0.5; 134 | } 135 | 136 | /* Giscus begone */ 137 | 138 | #__comments { 139 | display: none; 140 | } 141 | 142 | .giscus { 143 | margin-top: 64px; 144 | position: relative; 145 | margin-bottom: -64px; 146 | z-index: 100; 147 | } 148 | 149 | .giscus-frame { 150 | height: auto !important; 151 | } -------------------------------------------------------------------------------- /puro/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.5.0 2 | 3 | * Fixed several major bugs 4 | * Added native arm64 mac build 5 | 6 | ## 1.4.11 7 | 8 | * Bug fixes 9 | * Added `-d` flag to `puro ls` to show dart versions 10 | 11 | ## 1.4.10 12 | 13 | * Bug fixes 14 | 15 | ## 1.4.9 16 | 17 | * Bug fixes 18 | 19 | ## 1.4.8 20 | 21 | * Bug fixes 22 | * Support for newer versions of Flutter that use the monorepo 23 | 24 | ## 1.4.7 25 | 26 | * Bug fixes 27 | 28 | ## 1.4.6 29 | 30 | * Bug fixes 31 | 32 | ## 1.4.5 33 | 34 | * Bug fixes 35 | 36 | ## 1.4.4 37 | 38 | * Bug fixes 39 | 40 | ## 1.4.3 41 | 42 | * Absolutely nothing, the `+1` of the previous release broke CloudFront lol 43 | 44 | ## 1.4.2+1 45 | 46 | * Bug fixes 47 | 48 | ## 1.4.2 49 | 50 | * Bug fixes 51 | * Unused caches are now cleaned up automatically 52 | 53 | ## 1.4.1 54 | 55 | * Bug fixes 56 | * Added the `build-shell` command to open a shell for building the Flutter engine manually 57 | * Added the `--projects`/`-p` flag to `ls` to show which projects are using each environment 58 | 59 | ## 1.4.0 60 | 61 | * Bug fixes 62 | * Added support for versioned environments, you can now `puro use ` to switch to a specific version instead of 63 | creating one manually 64 | 65 | ## 1.3.8 66 | 67 | * Bug fixes 68 | 69 | ## 1.3.7 70 | 71 | * Bug fixes 72 | * The `ls` command now has a separate indicator for the global default 73 | * The `create` and `upgrade` commands now recognize tags without an official release 74 | 75 | ## 1.3.6 76 | 77 | * Bug fixes 78 | * Now using 7zip to speed up engine unzipping on Windows 79 | * Added 'downgrade' as an alias for the 'upgrade' command 80 | * Added the `rename` command 81 | * The `rm` command now warns if known projects are using the environment 82 | 83 | ## 1.3.5 84 | 85 | * Bug fixes 86 | 87 | ## 1.3.4 88 | 89 | * Bug fixes 90 | 91 | ## 1.3.3 92 | 93 | * Bug fixes 94 | 95 | ## 1.3.2 96 | 97 | * Bug fixes 98 | 99 | ## 1.3.1 100 | 101 | * Bug fixes 102 | 103 | ## 1.3.0 104 | 105 | * Bug fixes 106 | * Added the `uninstall-puro` command 107 | 108 | ## 1.2.6 109 | 110 | * Bug fixes 111 | 112 | ## 1.2.5 113 | 114 | * Bug fixes 115 | 116 | ## 1.2.4 117 | 118 | * Added the `--extra` option to to `eval` 119 | * Added the `repl` command 120 | 121 | ## 1.2.3 122 | 123 | * Bug fixes 124 | 125 | ## 1.2.2 126 | 127 | * Bug fixes 128 | 129 | ## 1.2.1 130 | 131 | * Bug fixes 132 | * Added package support to eval 133 | 134 | ## 1.2.0 135 | 136 | * Bug fixes 137 | * Added the `eval` command 138 | 139 | ## 1.1.13 140 | 141 | * Bug fixes 142 | 143 | ## 1.1.12 144 | 145 | * Bug fixes 146 | 147 | ## 1.1.11 148 | 149 | * Bug fixes 150 | * Added support for Google storage mirrors with FLUTTER_STORAGE_BASE_URL 151 | * Added the `engine prepare` command 152 | * Added the `engine build-env` command 153 | 154 | ## 1.1.10 155 | 156 | * Bug fixes 157 | 158 | ## 1.1.9 159 | 160 | * Bug fixes 161 | 162 | ## 1.1.8 163 | 164 | * Bug fixes 165 | 166 | ## 1.1.7 167 | 168 | * Bug fixes 169 | * Added global configuration for bash/zsh profiles 170 | * Improved Windows installer 171 | 172 | ## 1.1.6 173 | 174 | * Bug fixes 175 | 176 | ## 1.1.5 177 | 178 | * Bug fixes 179 | 180 | ## 1.1.4 181 | 182 | * Bug fixes 183 | * Improved installing puro inside symlinks and non-standard paths 184 | 185 | ## 1.1.3 186 | 187 | * Bug fixes 188 | * Added support for older versions of Flutter 189 | 190 | ## 1.1.2 191 | 192 | * Bug fixes 193 | 194 | ## 1.1.1 195 | 196 | * Bug fixes 197 | 198 | ## 1.1.0 199 | 200 | * Bug fixes 201 | * Added the `ls-versions` command 202 | * Added the `gc` command 203 | 204 | ## 1.0.1 205 | 206 | * Bug fixes 207 | * Better support for standalone and pub installs 208 | 209 | ## 1.0.0 210 | 211 | * Bug fixes 212 | * First stable release 213 | 214 | ## 0.6.1 215 | 216 | * Fix small issue with upgrades 217 | 218 | ## 0.6.0 219 | 220 | * Bug fixes 221 | * Added automatic update checks 222 | * Changed the default pub root to ~/.puro/shared/pub_cache 223 | * Upgrades now infer the branch 224 | * Added the `--fork` option to `puro create` 225 | 226 | ## 0.5.0 227 | 228 | * Bug fixes 229 | * Added the `pub` command 230 | * Added the `upgrade-puro` command 231 | * Added dart/flutter shims to .puro/bin 232 | * Improved bash/zsh profile updating mechanism 233 | * Added the hidden `install-puro` command 234 | * Added PATH conflict detection to the `version` command 235 | 236 | ## 0.4.1 237 | 238 | * Bug fixes 239 | * Added the `--global` flag to `puro use` to set the default environment 240 | * Implemented default environments 241 | 242 | ## 0.4.0 243 | 244 | * Bug fixes 245 | * Performance improvements 246 | * Added the `upgrade` command for upgrading environments 247 | * Added the `--vscode` and `--intellij` flags to `puro use` which override whether their configs are generated 248 | * Added the `version` command 249 | 250 | ## 0.3.0 251 | 252 | * Bug fixes 253 | 254 | ## 0.2.0 255 | 256 | * Bug fixes 257 | * Performance improvements 258 | 259 | ## 0.1.0 260 | 261 | * Added progress bars 262 | * Added puro as an executable 263 | 264 | ## 0.0.1 265 | 266 | Initial version 267 | -------------------------------------------------------------------------------- /puro/lib/src/commands/ls_versions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:clock/clock.dart'; 4 | import 'package:neoansi/neoansi.dart'; 5 | import 'package:pub_semver/pub_semver.dart'; 6 | 7 | import '../command.dart'; 8 | import '../command_result.dart'; 9 | import '../config.dart'; 10 | import '../env/releases.dart'; 11 | import '../extensions.dart'; 12 | import '../proto/flutter_releases.pb.dart'; 13 | import '../terminal.dart'; 14 | 15 | class LsVersionsCommand extends PuroCommand { 16 | LsVersionsCommand() { 17 | argParser.addFlag( 18 | 'full', 19 | aliases: ['all'], 20 | help: 'Prints all releases instead of the last 10', 21 | negatable: false, 22 | ); 23 | argParser.addFlag( 24 | 'force', 25 | abbr: 'f', 26 | help: 'Forces new releases to be fetched regardless of cache duration', 27 | negatable: false, 28 | ); 29 | } 30 | 31 | @override 32 | final name = 'ls-versions'; 33 | 34 | @override 35 | List get aliases => ['releases', 'ls-releases']; 36 | 37 | @override 38 | final description = 'Lists available Flutter versions'; 39 | 40 | @override 41 | Future run() async { 42 | final full = argResults!['full'] as bool; 43 | final force = argResults!['force'] as bool; 44 | 45 | FlutterReleasesModel? flutterVersions; 46 | if (!force) { 47 | flutterVersions = await getCachedFlutterReleases( 48 | scope: scope, 49 | unlessStale: true, 50 | ); 51 | } 52 | flutterVersions ??= await fetchFlutterReleases(scope: scope); 53 | 54 | final parsedVersions = {}; 55 | final sortedReleases = flutterVersions.releases.toList() 56 | ..sort((a, b) { 57 | return parsedVersions 58 | .putIfAbsent(b.version, () => tryParseVersion(b.version)!) 59 | .compareTo( 60 | parsedVersions.putIfAbsent( 61 | a.version, 62 | () => tryParseVersion(a.version)!, 63 | ), 64 | ); 65 | }); 66 | final now = clock.now(); 67 | 68 | List latestReleasesFor(String channel) { 69 | final releases = []; 70 | final candidates = sortedReleases.where((e) => e.channel == channel); 71 | Version? lastVersion; 72 | for (final release in candidates) { 73 | final version = parsedVersions[release.version]!; 74 | 75 | // Can contain duplicates for each architecture 76 | if (version == lastVersion) continue; 77 | 78 | final isPreviousPatch = 79 | lastVersion != null && 80 | version.major == lastVersion.major && 81 | version.minor == lastVersion.minor; 82 | lastVersion = version; 83 | if (releases.length >= 5 && isPreviousPatch && !full) { 84 | continue; 85 | } 86 | releases.add(release); 87 | if (releases.length >= 10 && !full) break; 88 | } 89 | return releases; 90 | } 91 | 92 | final channelReleases = >{ 93 | 'stable': latestReleasesFor('stable'), 94 | 'beta': latestReleasesFor('beta'), 95 | }; 96 | 97 | return BasicMessageResult.format((format) { 98 | List> formatReleases(List releases) { 99 | return [ 100 | for (final release in releases) 101 | [ 102 | 'Flutter ${release.version}', 103 | format.color(' | ', foregroundColor: Ansi8BitColor.grey), 104 | DateTime.parse( 105 | release.releaseDate, 106 | ).difference(now).pretty(before: '', abbr: true), 107 | format.color(' | ', foregroundColor: Ansi8BitColor.grey), 108 | release.hash.substring(0, 10), 109 | format.color(' | ', foregroundColor: Ansi8BitColor.grey), 110 | 'Dart ${release.dartSdkVersion.split(' ').first}', 111 | ], 112 | ]; 113 | } 114 | 115 | final formattedReleases = >>{ 116 | for (final entry in channelReleases.entries) 117 | entry.key: formatReleases(entry.value), 118 | }; 119 | 120 | final colWidths = List.generate( 121 | formattedReleases.values.first.first.length, 122 | (index) { 123 | return formattedReleases.values.fold(0, (n, rows) { 124 | return max( 125 | n, 126 | rows.fold( 127 | 0, 128 | (n, row) => max(n, stripAnsiEscapes(row[index]).length), 129 | ), 130 | ); 131 | }); 132 | }, 133 | ); 134 | 135 | return [ 136 | for (final entry in formattedReleases.entries) ...[ 137 | 'Latest ${entry.key} releases:', 138 | for (final row in entry.value) 139 | '${row.mapWithIndex((s, i) => padRightColored(s, colWidths[i])).join()}', 140 | '', 141 | ], 142 | ].join('\n').trim(); 143 | }, type: CompletionType.info); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /puro/lib/src/engine/prepare.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import '../command_result.dart'; 5 | import '../config.dart'; 6 | import '../env/create.dart'; 7 | import '../git.dart'; 8 | import '../logger.dart'; 9 | import '../process.dart'; 10 | import '../progress.dart'; 11 | import '../provider.dart'; 12 | import 'depot_tools.dart'; 13 | import 'worker.dart'; 14 | 15 | Future prepareEngineSystemDeps({required Scope scope}) async { 16 | await installDepotTools(scope: scope); 17 | 18 | if (Platform.isLinux) { 19 | await installLinuxWorkerPackages(scope: scope); 20 | } else if (Platform.isWindows) { 21 | await ensureWindowsPythonInstalled(scope: scope); 22 | } else { 23 | throw UnsupportedOSError(); 24 | } 25 | } 26 | 27 | /// Checks out and prepares the engine for building. 28 | Future prepareEngine({ 29 | required Scope scope, 30 | required EnvConfig environment, 31 | required String? ref, 32 | String? forkRemoteUrl, 33 | bool force = false, 34 | }) async { 35 | final git = GitClient.of(scope); 36 | final config = PuroConfig.of(scope); 37 | final log = PuroLogger.of(scope); 38 | 39 | await prepareEngineSystemDeps(scope: scope); 40 | 41 | ref ??= environment.flutter.engineVersion; 42 | ref ??= await getEngineVersionOfCommit( 43 | scope: scope, 44 | commit: await git.getCurrentCommitHash(repository: environment.flutterDir), 45 | ); 46 | 47 | if (ref == null) { 48 | throw AssertionError( 49 | 'Failed to detect engine version of environment `${environment.name}`\n' 50 | 'Does `${environment.flutter.engineVersionFile.path}` exist?', 51 | ); 52 | } 53 | 54 | final sharedRepository = config.sharedEngineDir; 55 | if (forkRemoteUrl != null || 56 | !await git.checkCommitExists(repository: sharedRepository, commit: ref)) { 57 | await fetchOrCloneShared( 58 | scope: scope, 59 | repository: sharedRepository, 60 | remoteUrl: config.engineGitUrl, 61 | ); 62 | } 63 | 64 | final origin = forkRemoteUrl ?? config.engineGitUrl; 65 | final upstream = forkRemoteUrl == null ? null : config.engineGitUrl; 66 | 67 | final remotes = { 68 | if (upstream != null) 'upstream': GitRemoteUrls.single(upstream), 69 | 'origin': GitRemoteUrls.single(origin), 70 | }; 71 | 72 | final repository = environment.engine.engineSrcDir; 73 | 74 | await ProgressNode.of(scope).wrap((scope, node) async { 75 | node.description = 'Initializing repository'; 76 | if (!repository.childDirectory('.git').existsSync()) { 77 | repository.createSync(recursive: true); 78 | await git.init(repository: repository); 79 | } 80 | final alternatesFile = repository 81 | .childDirectory('.git') 82 | .childDirectory('objects') 83 | .childDirectory('info') 84 | .childFile('alternates'); 85 | final sharedObjects = sharedRepository 86 | .childDirectory('.git') 87 | .childDirectory('objects'); 88 | alternatesFile.writeAsStringSync('${sharedObjects.path}\n'); 89 | await git.syncRemotes(repository: repository, remotes: remotes); 90 | await git.checkout(repository: repository, ref: ref); 91 | }); 92 | 93 | // This is Python, not JSON. 94 | environment.engine.gclientFile.writeAsStringSync('''solutions = [ 95 | { 96 | "managed": False, 97 | "name": "src/flutter", 98 | "url": ${jsonEncode(origin)}, 99 | "custom_deps": {}, 100 | "deps_file": "DEPS", 101 | "safesync_url": "", 102 | } 103 | ] 104 | cache_dir = ${jsonEncode(config.sharedGClientDir.path)} 105 | '''); 106 | 107 | await ProgressNode.of(scope).wrap((scope, node) async { 108 | node.description = 'Running gclient sync (this may take awhile)'; 109 | 110 | final envVars = await getEngineBuildEnvVars(scope: scope); 111 | 112 | final proc = await startProcess( 113 | scope, 114 | 'gclient', 115 | ['sync', '--verbose', '--verbose'], 116 | workingDirectory: environment.engineRootDir.path, 117 | runInShell: true, 118 | environment: envVars, 119 | ); 120 | 121 | final logFile = environment.engineRootDir.childFile('gclient.log'); 122 | final logSink = logFile.openWrite(mode: FileMode.append); 123 | final stdoutFuture = proc.stdout 124 | .transform(utf8.decoder) 125 | .transform(const LineSplitter()) 126 | .listen((line) { 127 | log.d('gclient: $line'); 128 | logSink.writeln(line); 129 | }) 130 | .asFuture(); 131 | final stderrFuture = proc.stderr 132 | .transform(utf8.decoder) 133 | .transform(const LineSplitter()) 134 | .listen((line) { 135 | log.d('(E) gclient: $line'); 136 | logSink.writeln(line); 137 | }) 138 | .asFuture(); 139 | 140 | final exitCode = await proc.exitCode; 141 | await stdoutFuture; 142 | await stderrFuture; 143 | await logSink.close(); 144 | 145 | if (exitCode != 0) { 146 | throw CommandError( 147 | 'gclient sync failed with exit code ${await proc.exitCode}\n' 148 | 'See ${logFile.path} for more details', 149 | ); 150 | } 151 | }); 152 | } 153 | -------------------------------------------------------------------------------- /puro/lib/src/logger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:clock/clock.dart'; 4 | import 'package:neoansi/neoansi.dart'; 5 | 6 | import 'extensions.dart'; 7 | import 'provider.dart'; 8 | import 'terminal.dart'; 9 | 10 | enum LogLevel { 11 | wtf, 12 | error, 13 | warning, 14 | verbose, 15 | debug; 16 | 17 | bool operator >(LogLevel other) { 18 | return index > other.index; 19 | } 20 | 21 | bool operator <(LogLevel other) { 22 | return index < other.index; 23 | } 24 | 25 | bool operator >=(LogLevel other) { 26 | return index >= other.index; 27 | } 28 | 29 | bool operator <=(LogLevel other) { 30 | return index <= other.index; 31 | } 32 | } 33 | 34 | class LogEntry { 35 | LogEntry(this.timestamp, this.level, this.message); 36 | 37 | final DateTime timestamp; 38 | final LogLevel level; 39 | final String message; 40 | } 41 | 42 | class PuroLogger { 43 | PuroLogger({this.level, this.terminal, this.onAdd, this.profile = false}); 44 | 45 | LogLevel? level; 46 | Terminal? terminal; 47 | void Function(LogEntry event)? onAdd; 48 | bool profile; 49 | 50 | late final stopwatch = Stopwatch(); 51 | 52 | OutputFormatter get format => terminal?.format ?? plainFormatter; 53 | 54 | bool shouldLog(LogLevel level) { 55 | return this.level != null && level <= this.level!; 56 | } 57 | 58 | void add(LogEntry event) { 59 | if (level == null || level! < event.level) return; 60 | _add(event); 61 | } 62 | 63 | void _add(LogEntry event) { 64 | if (onAdd != null) { 65 | onAdd!(event); 66 | } 67 | if (terminal != null) { 68 | final buf = StringBuffer(); 69 | 70 | if (profile) { 71 | final elapsed = stopwatch.elapsedMilliseconds; 72 | buf.write( 73 | format.color( 74 | '${elapsed.pretty().padLeft(4)} ', 75 | foregroundColor: elapsed > 1000 76 | ? Ansi8BitColor.red 77 | : elapsed > 100 78 | ? Ansi8BitColor.orange1 79 | : Ansi8BitColor.green, 80 | bold: true, 81 | ), 82 | ); 83 | if (!stopwatch.isRunning) { 84 | stopwatch.start(); 85 | } else { 86 | stopwatch.reset(); 87 | } 88 | } 89 | 90 | buf.write( 91 | format.color( 92 | levelPrefixes[event.level]!, 93 | foregroundColor: levelColors[event.level]!, 94 | bold: true, 95 | ), 96 | ); 97 | 98 | terminal!.writeln(format.prefix('$buf ', event.message)); 99 | } 100 | } 101 | 102 | String _interpolate(Object? message) { 103 | if (message is Function) { 104 | return '${message()}'; 105 | } else { 106 | return '$message'; 107 | } 108 | } 109 | 110 | void d(Object? message) { 111 | if (level == null || level! < LogLevel.debug) return; 112 | _add(LogEntry(DateTime.now(), LogLevel.debug, _interpolate(message))); 113 | } 114 | 115 | void v(Object? message) { 116 | if (level == null || level! < LogLevel.verbose) return; 117 | _add(LogEntry(DateTime.now(), LogLevel.verbose, _interpolate(message))); 118 | } 119 | 120 | void w(Object? message) { 121 | if (level == null || level! < LogLevel.warning) return; 122 | _add(LogEntry(DateTime.now(), LogLevel.warning, _interpolate(message))); 123 | } 124 | 125 | void e(Object? message) { 126 | if (level == null || level! < LogLevel.error) return; 127 | _add(LogEntry(DateTime.now(), LogLevel.error, _interpolate(message))); 128 | } 129 | 130 | void wtf(Object? message) { 131 | if (level == null || level! < LogLevel.wtf) return; 132 | _add(LogEntry(DateTime.now(), LogLevel.wtf, _interpolate(message))); 133 | } 134 | 135 | static const levelPrefixes = { 136 | LogLevel.wtf: '[WTF]', 137 | LogLevel.error: '[E]', 138 | LogLevel.warning: '[W]', 139 | LogLevel.verbose: '[V]', 140 | LogLevel.debug: '[D]', 141 | }; 142 | 143 | static const levelColors = { 144 | LogLevel.wtf: Ansi8BitColor.pink1, 145 | LogLevel.error: Ansi8BitColor.red, 146 | LogLevel.warning: Ansi8BitColor.orange1, 147 | LogLevel.verbose: Ansi8BitColor.grey62, 148 | LogLevel.debug: Ansi8BitColor.grey, 149 | }; 150 | 151 | static final provider = Provider.late(); 152 | static PuroLogger of(Scope scope) => scope.read(provider); 153 | } 154 | 155 | FutureOr runOptional( 156 | Scope scope, 157 | String action, 158 | Future fn(), { 159 | LogLevel level = LogLevel.error, 160 | LogLevel? exceptionLevel, 161 | bool skip = false, 162 | }) async { 163 | final log = PuroLogger.of(scope); 164 | if (skip) { 165 | log.v('Skipped $action'); 166 | return null; 167 | } 168 | final uppercaseAction = 169 | action.substring(0, 1).toUpperCase() + action.substring(1); 170 | log.v('$uppercaseAction...'); 171 | try { 172 | return await fn(); 173 | } catch (exception, stackTrace) { 174 | final time = clock.now(); 175 | log.add(LogEntry(time, level, 'Exception while $action')); 176 | log.add(LogEntry(time, exceptionLevel ?? level, '$exception\n$stackTrace')); 177 | return null; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /puro/lib/src/eval/parse.dart: -------------------------------------------------------------------------------- 1 | // The analyzer package is silly and hides important files 2 | // ignore_for_file: implementation_imports 3 | 4 | import 'package:analyzer/dart/analysis/features.dart'; 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import 'package:analyzer/dart/ast/token.dart'; 7 | import 'package:analyzer/dart/element/element.dart'; 8 | import 'package:analyzer/diagnostic/diagnostic.dart'; 9 | import 'package:analyzer/error/listener.dart'; 10 | import 'package:analyzer/exception/exception.dart'; 11 | import 'package:analyzer/source/line_info.dart'; 12 | import 'package:analyzer/src/dart/scanner/reader.dart'; 13 | import 'package:analyzer/src/dart/scanner/scanner.dart'; 14 | import 'package:analyzer/src/generated/parser.dart'; 15 | import 'package:analyzer/src/string_source.dart'; 16 | import 'package:pub_semver/pub_semver.dart'; 17 | 18 | class ParseResult { 19 | ParseResult({ 20 | required this.code, 21 | this.node, 22 | this.token, 23 | this.scanErrors = const [], 24 | this.scanException, 25 | this.parseErrors = const [], 26 | this.parseException, 27 | this.exhaustive = false, 28 | }); 29 | 30 | final String code; 31 | final T? node; 32 | final Token? token; 33 | final CaughtException? scanException; 34 | final CaughtException? parseException; 35 | final List scanErrors; 36 | final List parseErrors; 37 | final bool exhaustive; 38 | 39 | late final hasReturn = () { 40 | if (node is! CompilationUnit) return false; 41 | for (final decl in (node as CompilationUnit).declarations) { 42 | if (decl is FunctionDeclaration && decl.name.lexeme == 'main') { 43 | final returnType = decl.returnType?.toSource(); 44 | if (returnType == 'void' || returnType == 'Future') { 45 | return false; 46 | } 47 | } 48 | } 49 | return true; 50 | }(); 51 | 52 | bool get hasError => 53 | scanException != null || 54 | parseException != null || 55 | scanErrors.isNotEmpty || 56 | parseErrors.isNotEmpty; 57 | 58 | @override 59 | String toString() { 60 | return 'SimpleParseResult<$T>(' 61 | ' node: $node, ' 62 | ' token: $token, ' 63 | ' scanException: $scanException, ' 64 | ' parseException: $parseException, ' 65 | ' scanErrors: $scanErrors, ' 66 | ' parseErrors: $parseErrors, ' 67 | ' hasError: $hasError, ' 68 | ' exhaustive: $exhaustive' 69 | ')'; 70 | } 71 | } 72 | 73 | extension TokenExtension on Token { 74 | Token get last { 75 | if (next == this || next == null) return this; 76 | return next!.last; 77 | } 78 | } 79 | 80 | ParseResult parseDart( 81 | String code, 82 | T Function(Parser parser) fn, 83 | ) { 84 | final source = StringSource(code, '/eval.dart'); 85 | final scanErrors = _ErrorListener(); 86 | final reader = CharSequenceReader(code); 87 | final featureSet = FeatureSet.latestLanguageVersion(); 88 | final scanner = Scanner(source, reader, scanErrors) 89 | ..configureFeatures( 90 | featureSetForOverriding: featureSet, 91 | featureSet: featureSet, 92 | ); 93 | final Token token; 94 | try { 95 | token = scanner.tokenize(); 96 | } catch (exception, stackTrace) { 97 | return ParseResult( 98 | code: code, 99 | scanException: CaughtException(exception, stackTrace), 100 | ); 101 | } 102 | final parseErrors = _ErrorListener(); 103 | late final parser = Parser( 104 | source, 105 | languageVersion: LibraryLanguageVersion( 106 | package: Version(0, 0, 0), 107 | override: null, 108 | ), 109 | parseErrors, 110 | featureSet: featureSet, 111 | lineInfo: LineInfo.fromContent(code), 112 | )..currentToken = token; 113 | final node = fn(parser); 114 | return ParseResult( 115 | code: code, 116 | node: node, 117 | token: token, 118 | scanErrors: scanErrors.errors, 119 | parseErrors: parseErrors.errors, 120 | exhaustive: parser.currentToken.isEof, 121 | ); 122 | } 123 | 124 | ParseResult parseDartExpression( 125 | String code, { 126 | bool async = false, 127 | }) => parseDart(async ? '() async => $code' : code, (parser) { 128 | final expr = parser.parseExpression2(); 129 | if (async && expr is FunctionExpression) { 130 | return (expr.body as ExpressionFunctionBody).expression; 131 | } 132 | return expr; 133 | }); 134 | 135 | ParseResult parseDartCompilationUnit(String code) => 136 | parseDart(code, (parser) => parser.parseCompilationUnit2()); 137 | 138 | ParseResult parseDartBlock(String code) => parseDart( 139 | code, 140 | (parser) => 141 | (parser.parseFunctionBody( 142 | false, 143 | ParserErrorCode.missingFunctionBody, 144 | false, 145 | ) 146 | as BlockFunctionBody) 147 | .block, 148 | ); 149 | 150 | class _ErrorListener implements DiagnosticListener { 151 | final errors = []; 152 | 153 | @override 154 | void onDiagnostic(Diagnostic diagnostic) { 155 | errors.add(diagnostic); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /website/docs/assets/giscus_theme_dark.css: -------------------------------------------------------------------------------- 1 | /*! MIT License 2 | * Copyright (c) 2018 GitHub Inc. 3 | * https://github.com/primer/primitives/blob/main/LICENSE 4 | */ 5 | 6 | main { 7 | --color-prettylights-syntax-comment: #8b949e; 8 | --color-prettylights-syntax-constant: #79c0ff; 9 | --color-prettylights-syntax-entity: #d2a8ff; 10 | --color-prettylights-syntax-storage-modifier-import: #c9d1d9; 11 | --color-prettylights-syntax-entity-tag: #7ee787; 12 | --color-prettylights-syntax-keyword: #ff7b72; 13 | --color-prettylights-syntax-string: #a5d6ff; 14 | --color-prettylights-syntax-variable: #ffa657; 15 | --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; 16 | --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; 17 | --color-prettylights-syntax-invalid-illegal-bg: #8e1519; 18 | --color-prettylights-syntax-carriage-return-text: #f0f6fc; 19 | --color-prettylights-syntax-carriage-return-bg: #b62324; 20 | --color-prettylights-syntax-string-regexp: #7ee787; 21 | --color-prettylights-syntax-markup-list: #f2cc60; 22 | --color-prettylights-syntax-markup-heading: #1f6feb; 23 | --color-prettylights-syntax-markup-italic: #c9d1d9; 24 | --color-prettylights-syntax-markup-bold: #c9d1d9; 25 | --color-prettylights-syntax-markup-deleted-text: #ffdcd7; 26 | --color-prettylights-syntax-markup-deleted-bg: #67060c; 27 | --color-prettylights-syntax-markup-inserted-text: #aff5b4; 28 | --color-prettylights-syntax-markup-inserted-bg: #033a16; 29 | --color-prettylights-syntax-markup-changed-text: #ffdfb6; 30 | --color-prettylights-syntax-markup-changed-bg: #5a1e02; 31 | --color-prettylights-syntax-markup-ignored-text: #c9d1d9; 32 | --color-prettylights-syntax-markup-ignored-bg: #1158c7; 33 | --color-prettylights-syntax-meta-diff-range: #d2a8ff; 34 | --color-prettylights-syntax-brackethighlighter-angle: #8b949e; 35 | --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; 36 | --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; 37 | --color-btn-text: #c9d1d9; 38 | --color-btn-bg: #21262d; 39 | --color-btn-border: rgb(240 246 252 / 10%); 40 | --color-btn-shadow: 0 0 transparent; 41 | --color-btn-inset-shadow: 0 0 transparent; 42 | --color-btn-hover-bg: #30363d; 43 | --color-btn-hover-border: #8b949e; 44 | --color-btn-active-bg: hsl(212deg 12% 18% / 100%); 45 | --color-btn-active-border: #6e7681; 46 | --color-btn-selected-bg: #161b22; 47 | --color-btn-primary-text: #fff; 48 | --color-btn-primary-bg: #238636; 49 | --color-btn-primary-border: rgb(240 246 252 / 10%); 50 | --color-btn-primary-shadow: 0 0 transparent; 51 | --color-btn-primary-inset-shadow: 0 0 transparent; 52 | --color-btn-primary-hover-bg: #2ea043; 53 | --color-btn-primary-hover-border: rgb(240 246 252 / 10%); 54 | --color-btn-primary-selected-bg: #238636; 55 | --color-btn-primary-selected-shadow: 0 0 transparent; 56 | --color-btn-primary-disabled-text: rgb(255 255 255 / 50%); 57 | --color-btn-primary-disabled-bg: rgb(35 134 54 / 60%); 58 | --color-btn-primary-disabled-border: rgb(240 246 252 / 10%); 59 | --color-action-list-item-default-hover-bg: rgb(177 186 196 / 12%); 60 | --color-segmented-control-bg: rgb(110 118 129 / 10%); 61 | --color-segmented-control-button-bg: #0d1117; 62 | --color-segmented-control-button-selected-border: #6e7681; 63 | --color-fg-default: #e6edf3; 64 | --color-fg-muted: #7d8590; 65 | --color-fg-subtle: #6e7681; 66 | --color-canvas-default: #0d1117; 67 | --color-canvas-overlay: #161b22; 68 | --color-canvas-inset: #010409; 69 | --color-canvas-subtle: #161b22; 70 | --color-border-default: #30363d; 71 | --color-border-muted: #21262d; 72 | --color-neutral-muted: rgb(110 118 129 / 40%); 73 | --color-accent-fg: #2f81f7; 74 | --color-accent-emphasis: #1f6feb; 75 | --color-accent-muted: rgb(56 139 253 / 40%); 76 | --color-accent-subtle: rgb(56 139 253 / 10%); 77 | --color-success-fg: #3fb950; 78 | --color-attention-fg: #d29922; 79 | --color-attention-muted: rgb(187 128 9 / 40%); 80 | --color-attention-subtle: rgb(187 128 9 / 15%); 81 | --color-danger-fg: #f85149; 82 | --color-danger-muted: rgb(248 81 73 / 40%); 83 | --color-danger-subtle: rgb(248 81 73 / 10%); 84 | --color-primer-shadow-inset: 0 0 transparent; 85 | --color-scale-gray-7: #21262d; 86 | --color-scale-blue-8: #0c2d6b; 87 | 88 | /*! Extensions from @primer/css/alerts/flash.scss */ 89 | --color-social-reaction-bg-hover: var(--color-scale-gray-7); 90 | --color-social-reaction-bg-reacted-hover: var(--color-scale-blue-8); 91 | } 92 | 93 | main .pagination-loader-container { 94 | background-image: url("https://github.com/images/modules/pulls/progressive-disclosure-line-dark.svg"); 95 | } 96 | 97 | main .gsc-loading-image { 98 | background-image: url("https://github.githubassets.com/images/mona-loading-dark.gif"); 99 | } 100 | 101 | /* Giscus begone */ 102 | 103 | .gsc-comments { 104 | display: none; 105 | } 106 | 107 | .gsc-reactions { 108 | padding-bottom: 64px; 109 | } 110 | 111 | body { 112 | height: 200px; 113 | } -------------------------------------------------------------------------------- /website/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: true 3 | hide: 4 | - navigation 5 | --- 6 | 7 | # About 8 | 9 | Puro is a powerful tool for installing and upgrading [Flutter](https://flutter.dev/) versions, it is essential for 10 | developers that work on multiple projects or have slower internet. 11 | 12 | With Puro you can: 13 | 14 | * Use different versions of Flutter at the same time 15 | { ^ .star-list } 16 | * Download new versions twice as fast with significantly less disk space and internet bandwidth 17 | * Use versions globally or per-project 18 | * Automatically configure IDE settings with a single command 19 | 20 | ## Installation 21 | 22 | === "Windows" 23 | 24 | Puro can be installed on Windows with PowerShell (as your current user, **do not use administrator**): 25 | 26 | ```ps1 27 | Invoke-WebRequest -Uri "https://puro.dev/builds/master/windows-x64/puro.exe" -OutFile "$env:temp\puro.exe"; &"$env:temp\puro.exe" install-puro --promote 28 | ``` 29 | 30 | Or as a standalone executable (not recommended): 31 | 32 | [:material-console: Standalone](https://puro.dev/builds/master/windows-x64/puro.exe){ .md-button } 33 | 34 | --- 35 | 36 | note: Flutter requires [git](https://git-scm.com/) which can be installed at [https://git-scm.com/download/win](https://git-scm.com/download/win) 37 | 38 | === "Linux" 39 | 40 | Puro can be installed on Linux with the following command: 41 | 42 | ```sh 43 | curl -o- https://puro.dev/install.sh | PURO_VERSION="master" bash 44 | ``` 45 | 46 | Or as a standalone executable (not recommended): 47 | 48 | [:material-console: Standalone](https://puro.dev/builds/master/linux-x64/puro){ .md-button } 49 | 50 | --- 51 | 52 | note: Flutter requires [git](https://git-scm.com/) which can be installed with most package managers e.g. apt: 53 | 54 | ```sh 55 | sudo apt install git 56 | ``` 57 | 58 | === "Mac" 59 | 60 | Puro can be installed on Mac with the following command: 61 | 62 | ```sh 63 | curl -o- https://puro.dev/install.sh | PURO_VERSION="master" bash 64 | ``` 65 | 66 | Or as a standalone executable (not recommended): 67 | 68 | [:material-console: Standalone](https://puro.dev/builds/master/darwin-x64/puro){ .md-button } 69 | 70 | --- 71 | 72 | note: Flutter requires [git](https://git-scm.com/) which can be installed with [Homebrew](https://brew.sh/) for macOS: 73 | 74 | ```sh 75 | brew install git 76 | ``` 77 | 78 | 79 | 80 | ## Quick Start 81 | 82 | After installing Puro you can run `puro flutter doctor` to install the latest stable version of Flutter, if you want to 83 | switch to beta you can run `puro use -g beta` and then `puro flutter doctor` again. 84 | 85 | And that's it, you're ready to go! 86 | 87 | Puro uses the concept of "environments" to manage Flutter versions, these can either be tied to a specific version / 88 | release channel, or a named environment that can be upgraded independently. 89 | 90 | Environments can be set globally or per-project, the global environment is set to `stable` by default. 91 | 92 | Cheat sheet: 93 | 94 | ``` 95 | # Create a new environment "foo" with the latest stable release 96 | puro create foo stable 97 | 98 | # Create a new environment "bar" with with Flutter 3.13.6 99 | puro create bar 3.13.6 100 | 101 | # Switch "bar" to a specific Flutter version 102 | puro upgrade bar 3.10.6 103 | 104 | # List available environments 105 | puro ls 106 | 107 | # List available Flutter releases 108 | puro releases 109 | 110 | # Switch the current project to use "foo" 111 | puro use foo 112 | 113 | # Switch the global default to "bar" 114 | puro use -g bar 115 | 116 | # Remove puro configuration from the current project 117 | puro clean 118 | 119 | # Delete the "foo" environment 120 | puro rm foo 121 | 122 | # Run flutter commands in a specific environment 123 | puro -e foo flutter ... 124 | puro -e foo dart ... 125 | puro -e foo pub ... 126 | ``` 127 | 128 | See the full command list at https://puro.dev/reference/manual/ 129 | 130 | ## Performance 131 | 132 | Puro implements a few optimizations that make installing Flutter as fast as possible. 133 | First-time installations are 20% faster while improving subsequent installations by a whopping 50-95%: 134 | 135 | ![](assets/install_time_comparison.svg) 136 | 137 | This also translates into much lower network usage: 138 | 139 | ![](assets/network_usage_comparison.svg) 140 | 141 | ## How it works 142 | 143 | Puro achieves these performance gains with a few smart optimizations: 144 | 145 | * Parallel git clone and engine download 146 | * Global cache for git history 147 | * Global cache for engine versions 148 | 149 | With other approaches, each Flutter repository is in its own folder, requiring you to download and store the git history, engine, and framework of each version: 150 | 151 | ![](assets/storage_without_puro.png) 152 | 153 | Puro implements a technology similar to GitLab's [object deduplication](https://docs.gitlab.com/ee/development/git_object_deduplication.html) to avoid downloading the same git objects over and over again. It also uses symlinks to share the same engine version between multiple installations: 154 | 155 | ![](assets/storage_with_puro.png) 156 | -------------------------------------------------------------------------------- /website/docs/assets/giscus_theme_light.css: -------------------------------------------------------------------------------- 1 | /*! MIT License 2 | * Copyright (c) 2018 GitHub Inc. 3 | * https://github.com/primer/primitives/blob/main/LICENSE 4 | */ 5 | 6 | main { 7 | --color-prettylights-syntax-comment: #6e7781; 8 | --color-prettylights-syntax-constant: #0550ae; 9 | --color-prettylights-syntax-entity: #8250df; 10 | --color-prettylights-syntax-storage-modifier-import: #24292f; 11 | --color-prettylights-syntax-entity-tag: #116329; 12 | --color-prettylights-syntax-keyword: #cf222e; 13 | --color-prettylights-syntax-string: #0a3069; 14 | --color-prettylights-syntax-variable: #953800; 15 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; 16 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; 17 | --color-prettylights-syntax-invalid-illegal-bg: #82071e; 18 | --color-prettylights-syntax-carriage-return-text: #f6f8fa; 19 | --color-prettylights-syntax-carriage-return-bg: #cf222e; 20 | --color-prettylights-syntax-string-regexp: #116329; 21 | --color-prettylights-syntax-markup-list: #3b2300; 22 | --color-prettylights-syntax-markup-heading: #0550ae; 23 | --color-prettylights-syntax-markup-italic: #24292f; 24 | --color-prettylights-syntax-markup-bold: #24292f; 25 | --color-prettylights-syntax-markup-deleted-text: #82071e; 26 | --color-prettylights-syntax-markup-deleted-bg: #ffebe9; 27 | --color-prettylights-syntax-markup-inserted-text: #116329; 28 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1; 29 | --color-prettylights-syntax-markup-changed-text: #953800; 30 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5; 31 | --color-prettylights-syntax-markup-ignored-text: #eaeef2; 32 | --color-prettylights-syntax-markup-ignored-bg: #0550ae; 33 | --color-prettylights-syntax-meta-diff-range: #8250df; 34 | --color-prettylights-syntax-brackethighlighter-angle: #57606a; 35 | --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; 36 | --color-prettylights-syntax-constant-other-reference-link: #0a3069; 37 | --color-btn-text: #24292f; 38 | --color-btn-bg: #f6f8fa; 39 | --color-btn-border: rgb(31 35 40 / 15%); 40 | --color-btn-shadow: 0 1px 0 rgb(31 35 40 / 4%); 41 | --color-btn-inset-shadow: inset 0 1px 0 rgb(255 255 255 / 25%); 42 | --color-btn-hover-bg: #f3f4f6; 43 | --color-btn-hover-border: rgb(31 35 40 / 15%); 44 | --color-btn-active-bg: hsl(220deg 14% 93% / 100%); 45 | --color-btn-active-border: rgb(31 35 40 / 15%); 46 | --color-btn-selected-bg: hsl(220deg 14% 94% / 100%); 47 | --color-btn-primary-text: #fff; 48 | --color-btn-primary-bg: #1f883d; 49 | --color-btn-primary-border: rgb(31 35 40 / 15%); 50 | --color-btn-primary-shadow: 0 1px 0 rgb(31 35 40 / 10%); 51 | --color-btn-primary-inset-shadow: inset 0 1px 0 rgb(255 255 255 / 3%); 52 | --color-btn-primary-hover-bg: #1a7f37; 53 | --color-btn-primary-hover-border: rgb(31 35 40 / 15%); 54 | --color-btn-primary-selected-bg: hsl(137deg 66% 28% / 100%); 55 | --color-btn-primary-selected-shadow: inset 0 1px 0 rgb(0 45 17 / 20%); 56 | --color-btn-primary-disabled-text: rgb(255 255 255 / 80%); 57 | --color-btn-primary-disabled-bg: #94d3a2; 58 | --color-btn-primary-disabled-border: rgb(31 35 40 / 15%); 59 | --color-action-list-item-default-hover-bg: rgb(208 215 222 / 32%); 60 | --color-segmented-control-bg: #eaeef2; 61 | --color-segmented-control-button-bg: #fff; 62 | --color-segmented-control-button-selected-border: #8c959f; 63 | --color-fg-default: #1F2328; 64 | --color-fg-muted: #656d76; 65 | --color-fg-subtle: #6e7781; 66 | --color-canvas-default: #fff; 67 | --color-canvas-overlay: #fff; 68 | --color-canvas-inset: #f6f8fa; 69 | --color-canvas-subtle: #f6f8fa; 70 | --color-border-default: #d0d7de; 71 | --color-border-muted: hsl(210deg 18% 87% / 100%); 72 | --color-neutral-muted: rgb(175 184 193 / 20%); 73 | --color-accent-fg: #0969da; 74 | --color-accent-emphasis: #0969da; 75 | --color-accent-muted: rgb(84 174 255 / 40%); 76 | --color-accent-subtle: #ddf4ff; 77 | --color-success-fg: #1a7f37; 78 | --color-attention-fg: #9a6700; 79 | --color-attention-muted: rgb(212 167 44 / 40%); 80 | --color-attention-subtle: #fff8c5; 81 | --color-danger-fg: #d1242f; 82 | --color-danger-muted: rgb(255 129 130 / 40%); 83 | --color-danger-subtle: #ffebe9; 84 | --color-primer-shadow-inset: inset 0 1px 0 rgb(208 215 222 / 20%); 85 | --color-scale-gray-1: #eaeef2; 86 | --color-scale-blue-1: #b6e3ff; 87 | 88 | /*! Extensions from @primer/css/alerts/flash.scss */ 89 | --color-social-reaction-bg-hover: var(--color-scale-gray-1); 90 | --color-social-reaction-bg-reacted-hover: var(--color-scale-blue-1); 91 | } 92 | 93 | main .pagination-loader-container { 94 | background-image: url("https://github.com/images/modules/pulls/progressive-disclosure-line.svg"); 95 | } 96 | 97 | main .gsc-loading-image { 98 | background-image: url("https://github.githubassets.com/images/mona-loading-default.gif"); 99 | } 100 | 101 | /* Giscus begone */ 102 | 103 | .gsc-comments { 104 | display: none; 105 | } 106 | 107 | .gsc-reactions { 108 | padding-bottom: 64px; 109 | } 110 | 111 | body { 112 | height: 200px; 113 | } -------------------------------------------------------------------------------- /puro/lib/src/workspace/install.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | 3 | import '../config.dart'; 4 | import '../env/default.dart'; 5 | import '../extensions.dart'; 6 | import '../logger.dart'; 7 | import '../provider.dart'; 8 | import 'common.dart'; 9 | import 'gitignore.dart'; 10 | import 'intellij.dart'; 11 | import 'vscode.dart'; 12 | 13 | /// Modifies IntelliJ and VSCode configs of the current project to use the 14 | /// selected environment's Flutter SDK. 15 | Future installIdeConfigs({ 16 | required Scope scope, 17 | required Directory projectDir, 18 | required EnvConfig environment, 19 | required ProjectConfig projectConfig, 20 | bool? vscode, 21 | bool? intellij, 22 | EnvConfig? replaceOnly, 23 | }) async { 24 | final log = PuroLogger.of(scope); 25 | log.d('vscode override: $vscode'); 26 | log.d('intellij override: $vscode'); 27 | await runOptional(scope, 'installing IntelliJ config', () async { 28 | final ideConfig = await IntelliJConfig.load( 29 | scope: scope, 30 | projectDir: projectDir, 31 | projectConfig: projectConfig, 32 | ); 33 | log.d('intellij exists: ${ideConfig.exists}'); 34 | if ((ideConfig.exists || intellij == true) && 35 | (replaceOnly == null || 36 | (ideConfig.dartSdkDir?.absolute.pathEquals( 37 | replaceOnly.flutter.cache.dartSdkDir, 38 | ) == 39 | true))) { 40 | await installIdeConfig( 41 | scope: scope, 42 | ideConfig: ideConfig, 43 | environment: environment, 44 | ); 45 | } 46 | }, skip: intellij == false); 47 | 48 | await runOptional(scope, 'installing VSCode config', () async { 49 | final ideConfig = await VSCodeConfig.load( 50 | scope: scope, 51 | projectDir: projectDir, 52 | projectConfig: projectConfig, 53 | ); 54 | log.d('vscode exists: ${ideConfig.exists}'); 55 | if ((ideConfig.exists || vscode == true) && 56 | (replaceOnly == null || 57 | (ideConfig.dartSdkDir?.absolute.pathEquals( 58 | replaceOnly.flutter.cache.dartSdkDir, 59 | ) == 60 | true))) { 61 | await installIdeConfig( 62 | scope: scope, 63 | ideConfig: ideConfig, 64 | environment: environment, 65 | ); 66 | } 67 | }, skip: vscode == false); 68 | } 69 | 70 | Future installIdeConfig({ 71 | required Scope scope, 72 | required IdeConfig ideConfig, 73 | required EnvConfig environment, 74 | }) async { 75 | final flutterSdkPath = environment.flutterDir.path; 76 | final dartSdkPath = environment.flutter.cache.dartSdkDir.path; 77 | final log = PuroLogger.of(scope); 78 | log.v('Workspace path: `${ideConfig.workspaceDir.path}`'); 79 | if (ideConfig.flutterSdkDir?.path != flutterSdkPath || 80 | (ideConfig.dartSdkDir != null && 81 | ideConfig.dartSdkDir?.path != dartSdkPath)) { 82 | log.v('Configuring ${ideConfig.name}...'); 83 | await ideConfig.backup(scope: scope); 84 | ideConfig.dartSdkDir = environment.flutter.cache.dartSdkDir; 85 | ideConfig.flutterSdkDir = environment.flutterDir; 86 | await ideConfig.save(scope: scope); 87 | } else { 88 | log.v('${environment.name} already configured'); 89 | } 90 | } 91 | 92 | /// Installs gitignores and IDE configs to [projectDir}. 93 | Future installWorkspaceEnvironment({ 94 | required Scope scope, 95 | required Directory projectDir, 96 | required EnvConfig environment, 97 | required ProjectConfig projectConfig, 98 | bool? vscode, 99 | bool? intellij, 100 | EnvConfig? replaceOnly, 101 | }) async { 102 | await runOptional( 103 | scope, 104 | 'updating gitignore', 105 | () => updateGitignore( 106 | scope: scope, 107 | projectDir: projectDir, 108 | ignores: gitIgnoredFilesForWorkspace, 109 | ), 110 | ); 111 | await runOptional( 112 | scope, 113 | 'installing IDE configs', 114 | () => installIdeConfigs( 115 | scope: scope, 116 | projectDir: projectDir, 117 | environment: environment, 118 | vscode: vscode, 119 | intellij: intellij, 120 | projectConfig: projectConfig, 121 | replaceOnly: replaceOnly, 122 | ), 123 | ); 124 | } 125 | 126 | /// Switches the environment of the current project. 127 | Future switchEnvironment({ 128 | required Scope scope, 129 | required String? envName, 130 | required ProjectConfig projectConfig, 131 | Directory? projectDir, 132 | bool? vscode, 133 | bool? intellij, 134 | bool passive = false, 135 | }) async { 136 | final config = PuroConfig.of(scope); 137 | projectDir ??= config.project.ensureParentProjectDir(); 138 | final model = config.project.readDotfile(); 139 | final environment = await getProjectEnvOrDefault( 140 | scope: scope, 141 | envName: envName, 142 | ); 143 | final oldEnv = model.env; 144 | model.env = environment.name; 145 | await config.project.writeDotfile(scope, model); 146 | await installWorkspaceEnvironment( 147 | scope: scope, 148 | projectDir: projectDir, 149 | environment: environment, 150 | vscode: vscode, 151 | intellij: intellij, 152 | projectConfig: projectConfig, 153 | replaceOnly: passive && model.hasEnv() ? config.getEnv(oldEnv) : null, 154 | ); 155 | return environment; 156 | } 157 | -------------------------------------------------------------------------------- /puro/lib/src/json_edit/grammar.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | import 'element.dart'; 4 | 5 | class JsonGrammar extends GrammarDefinition> { 6 | static Token parse(String input) => 7 | JsonGrammar().build().parse(input).value; 8 | 9 | @override 10 | Parser> start() => ref0(element).end(); 11 | 12 | Parser> element() => [ 13 | literalElement(), 14 | mapElement(), 15 | arrayElement(), 16 | ].toChoiceParser(failureJoiner: selectFarthestJoined).cast(); 17 | 18 | Parser lineComment() { 19 | return (string('//') & Token.newlineParser().neg().star()).flatten(); 20 | } 21 | 22 | Parser inlineComment() { 23 | return (string('/*') & any().starLazy(string('*/')) & string('*/')) 24 | .flatten(); 25 | } 26 | 27 | Parser space() { 28 | return (whitespace() | lineComment() | inlineComment()).star().flatten(); 29 | } 30 | 31 | Parser> token(Parser parser) { 32 | return (ref0(space) & parser.token() & ref0(space)).token().map(( 33 | token, 34 | ) { 35 | final res = token.value; 36 | final leading = res[0] as String; 37 | final body = res[1] as Token; 38 | final trailing = res[2] as String; 39 | if (leading.isEmpty && trailing.isEmpty) { 40 | return body; 41 | } else { 42 | return Token( 43 | JsonWhitespace(leading: leading, body: body, trailing: trailing), 44 | token.buffer, 45 | token.start, 46 | token.stop, 47 | ); 48 | } 49 | }); 50 | } 51 | 52 | Parser escapedChar() => 53 | (char(r'\') & pattern(escapeChars.keys.join())) 54 | .pick(1) 55 | .map((Object? str) => escapeChars[str]!); 56 | 57 | Parser unicodeChar() => 58 | (string(r'\u') & pattern('0-9A-Fa-f').times(4)).map((digits) { 59 | final charCode = int.parse((digits[1] as List).join(), radix: 16); 60 | return String.fromCharCode(charCode); 61 | }); 62 | 63 | Parser stringLiteral() { 64 | return (char('"') & 65 | (pattern(r'^"\') | 66 | ref0(escapedChar) | 67 | ref0(unicodeChar)) 68 | .star() 69 | .map((list) => list.join()) & 70 | char('"')) 71 | .pick(1) 72 | .cast(); 73 | } 74 | 75 | Parser trueLiteral() => string('true').map((_) => true); 76 | Parser falseLiteral() => string('false').map((_) => false); 77 | Parser nullLiteral() => string('null').map((_) {}); 78 | Parser numLiteral() => 79 | (char('-').optional() & 80 | char('0').or(digit().plus()) & 81 | char('.').seq(digit().plus()).optional() & 82 | pattern( 83 | 'eE', 84 | ).seq(pattern('-+').optional()).seq(digit().plus()).optional()) 85 | .flatten() 86 | .map(num.parse); 87 | 88 | Parser> literalElement() { 89 | return token( 90 | [ 91 | ref0(trueLiteral), 92 | ref0(falseLiteral), 93 | ref0(nullLiteral), 94 | ref0(numLiteral), 95 | ref0(stringLiteral), 96 | ].toChoiceParser().token().map((str) => JsonLiteral(value: str)), 97 | ); 98 | } 99 | 100 | Parser> mapElement() { 101 | return token( 102 | (char('{') & 103 | ref0( 104 | mapEntryElement, 105 | ).plusSeparated(char(',')).map((e) => e.elements).optional() & 106 | ref0(space) & 107 | char(',').optional() & 108 | ref0(space) & 109 | char('}')) 110 | .map((res) { 111 | return JsonMap( 112 | children: (res[1] as List? ?? []) 113 | .cast>() 114 | .toList(), 115 | space: res[2] as String, 116 | ); 117 | }), 118 | ); 119 | } 120 | 121 | Parser> arrayElement() { 122 | return token( 123 | (char('[') & 124 | ref0( 125 | element, 126 | ).plusSeparated(char(',')).map((e) => e.elements).optional() & 127 | ref0(space) & 128 | char(',').optional() & 129 | ref0(space) & 130 | char(']')) 131 | .map((res) { 132 | return JsonArray( 133 | children: (res[1] as List? ?? []) 134 | .cast>() 135 | .toList(), 136 | space: res[2] as String, 137 | ); 138 | }), 139 | ); 140 | } 141 | 142 | Parser> mapEntryElement() { 143 | return (ref0(space) & 144 | ref0(stringLiteral).token() & 145 | ref0(space) & 146 | char(':') & 147 | ref0(element)) 148 | .map((res) { 149 | return JsonMapEntry( 150 | beforeKey: res[0] as String, 151 | key: res[1] as Token, 152 | afterKey: res[2] as String, 153 | value: res[4] as Token, 154 | ); 155 | }) 156 | .token(); 157 | } 158 | 159 | static const escapeChars = { 160 | '"': '"', 161 | r'\': r'\', 162 | '/': '/', 163 | 'b': '\b', 164 | 'f': '\f', 165 | 'n': '\n', 166 | 'r': '\r', 167 | 't': '\t', 168 | }; 169 | } 170 | -------------------------------------------------------------------------------- /puro/lib/src/env/upgrade.dart: -------------------------------------------------------------------------------- 1 | import '../command_result.dart'; 2 | import '../config.dart'; 3 | import '../git.dart'; 4 | import '../logger.dart'; 5 | import '../proto/puro.pb.dart'; 6 | import '../provider.dart'; 7 | import 'create.dart'; 8 | import 'env_shims.dart'; 9 | import 'flutter_tool.dart'; 10 | import 'version.dart'; 11 | 12 | class EnvUpgradeResult extends CommandResult { 13 | EnvUpgradeResult({ 14 | required this.environment, 15 | required this.from, 16 | required this.to, 17 | required this.forkRemoteUrl, 18 | this.switchedBranch = false, 19 | required this.toolInfo, 20 | }); 21 | 22 | final EnvConfig environment; 23 | final FlutterVersion from; 24 | final FlutterVersion to; 25 | final String? forkRemoteUrl; 26 | final bool switchedBranch; 27 | final FlutterToolInfo toolInfo; 28 | 29 | @override 30 | bool get success => true; 31 | 32 | bool get downgrade => from > to; 33 | 34 | @override 35 | CommandMessage get message => CommandMessage.format( 36 | (format) => from.commit == to.commit 37 | ? toolInfo.didUpdateTool || toolInfo.didUpdateEngine 38 | ? 'Finished installation of $to in environment `${environment.name}`' 39 | : 'Environment `${environment.name}` is already up to date' 40 | : '${downgrade ? 'Downgraded' : 'Upgraded'} environment `${environment.name}`\n' 41 | '${from.toString(format)} => ${to.toString(format)}', 42 | ); 43 | 44 | @override 45 | late final model = CommandResultModel( 46 | success: true, 47 | environmentUpgrade: EnvironmentUpgradeModel( 48 | name: environment.name, 49 | from: from.toModel(), 50 | to: to.toModel(), 51 | ), 52 | ); 53 | } 54 | 55 | /// Upgrades an environment to a different version of flutter. 56 | Future upgradeEnvironment({ 57 | required Scope scope, 58 | required EnvConfig environment, 59 | required FlutterVersion toVersion, 60 | bool force = false, 61 | }) async { 62 | final log = PuroLogger.of(scope); 63 | final git = GitClient.of(scope); 64 | environment.ensureExists(); 65 | 66 | if (isValidVersion(environment.name) && 67 | (toVersion.version == null || 68 | environment.name != '${toVersion.version}')) { 69 | throw CommandError( 70 | 'Cannot upgrade environment ${environment.name} to a different version, ' 71 | 'run `puro use ${toVersion.name}` instead to switch your project', 72 | ); 73 | } 74 | 75 | log.v('Upgrading environment in ${environment.envDir.path}'); 76 | 77 | final repository = environment.flutterDir; 78 | final currentCommit = await git.getCurrentCommitHash(repository: repository); 79 | 80 | final branch = await git.getBranch(repository: repository); 81 | var prefs = await environment.readPrefs(scope: scope); 82 | final fromVersion = prefs.hasDesiredVersion() 83 | ? FlutterVersion.fromModel(prefs.desiredVersion) 84 | : await getEnvironmentFlutterVersion( 85 | scope: scope, 86 | environment: environment, 87 | ); 88 | 89 | if (fromVersion == null) { 90 | throw CommandError("Couldn't find Flutter version, corrupt environment?"); 91 | } 92 | 93 | if (currentCommit != toVersion.commit || 94 | (toVersion.branch != null && branch != toVersion.branch)) { 95 | prefs = await environment.updatePrefs( 96 | scope: scope, 97 | fn: (prefs) { 98 | prefs.desiredVersion = toVersion.toModel(); 99 | }, 100 | ); 101 | 102 | if (prefs.hasForkRemoteUrl()) { 103 | if (branch == null) { 104 | throw CommandError( 105 | 'HEAD is not attached to a branch, could not upgrade fork', 106 | ); 107 | } 108 | if (await git.hasUncomittedChanges(repository: repository)) { 109 | throw CommandError("Can't upgrade fork with uncomitted changes"); 110 | } 111 | await git.pull(repository: repository, all: true); 112 | final switchBranch = 113 | toVersion.branch != null && branch != toVersion.branch; 114 | if (switchBranch) { 115 | await git.checkout(repository: repository, ref: toVersion.branch!); 116 | } 117 | await git.merge( 118 | repository: repository, 119 | fromCommit: toVersion.commit, 120 | fastForwardOnly: true, 121 | ); 122 | 123 | final toolInfo = await setUpFlutterTool( 124 | scope: scope, 125 | environment: environment, 126 | environmentPrefs: prefs, 127 | ); 128 | 129 | return EnvUpgradeResult( 130 | environment: environment, 131 | from: fromVersion, 132 | to: toVersion, 133 | forkRemoteUrl: prefs.forkRemoteUrl, 134 | switchedBranch: switchBranch, 135 | toolInfo: toolInfo, 136 | ); 137 | } 138 | 139 | await cloneFlutterWithSharedRefs( 140 | scope: scope, 141 | repository: environment.flutterDir, 142 | flutterVersion: toVersion, 143 | environment: environment, 144 | forkRemoteUrl: prefs.hasForkRemoteUrl() ? prefs.forkRemoteUrl : null, 145 | force: force, 146 | ); 147 | } 148 | 149 | // Replace flutter/dart with shims 150 | await installEnvShims(scope: scope, environment: environment); 151 | 152 | final toolInfo = await setUpFlutterTool( 153 | scope: scope, 154 | environment: environment, 155 | ); 156 | 157 | if (environment.flutter.legacyVersionFile.existsSync()) { 158 | environment.flutter.legacyVersionFile.deleteSync(); 159 | } 160 | 161 | return EnvUpgradeResult( 162 | environment: environment, 163 | from: fromVersion, 164 | to: toVersion, 165 | forkRemoteUrl: null, 166 | toolInfo: toolInfo, 167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /puro/lib/src/json_edit/element.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:petitparser/core.dart'; 4 | 5 | abstract class JsonElement { 6 | Object? toJson(); 7 | MapEntry toMapEntry() => 8 | throw ArgumentError('$runtimeType is not a map entry'); 9 | Iterable> get children; 10 | String toJsonString(); 11 | } 12 | 13 | String _indentString( 14 | String input, [ 15 | bool skipFirstLine = false, 16 | int indent = 1, 17 | ]) { 18 | final lines = input.split('\n'); 19 | if (skipFirstLine) { 20 | if (lines.length == 1) return lines.first; 21 | return '${lines.first}\n${lines.skip(1).map((e) => '${' ' * indent}$e').join('\n')}'; 22 | } else { 23 | return lines.map((e) => ' $e').join('\n'); 24 | } 25 | } 26 | 27 | class JsonWhitespace extends JsonElement { 28 | JsonWhitespace({ 29 | required this.leading, 30 | required this.body, 31 | required this.trailing, 32 | }); 33 | 34 | String leading; 35 | Token body; 36 | String trailing; 37 | 38 | @override 39 | Iterable> get children => [body]; 40 | 41 | @override 42 | Object? toJson() => body.value.toJson(); 43 | 44 | @override 45 | MapEntry toMapEntry() => body.value.toMapEntry(); 46 | 47 | @override 48 | String toString() { 49 | return 'Whitespace(\n' 50 | ' leading: ${jsonEncode(leading)}\n' 51 | ' body: ${_indentString('${body.value}', true)}\n' 52 | ' trailing: ${jsonEncode(trailing)}\n' 53 | ')'; 54 | } 55 | 56 | @override 57 | String toJsonString() => '$leading${body.value.toJsonString()}$trailing'; 58 | } 59 | 60 | class JsonMap extends JsonElement { 61 | JsonMap({required this.children, required this.space}); 62 | 63 | @override 64 | final List> children; 65 | final String space; 66 | 67 | Token? operator [](String key) { 68 | return children.cast?>().firstWhere( 69 | (child) => child!.value.key.value == key, 70 | orElse: () => null, 71 | ); 72 | } 73 | 74 | @override 75 | Object? toJson() { 76 | return Map.fromEntries([ 77 | for (final child in children) child.value.toMapEntry(), 78 | ]); 79 | } 80 | 81 | @override 82 | String toString() { 83 | if (space.isNotEmpty) { 84 | if (children.isEmpty) { 85 | return 'JsonMap(children: [], space: ${jsonEncode(space)})'; 86 | } 87 | return 'JsonMap(\n' 88 | ' children: [\n' 89 | ' ${children.map((e) => '${_indentString('${e.value}', false, 2)}\n').join()}' 90 | ' ],\n' 91 | ' space: ${jsonEncode(space)}\n' 92 | ')'; 93 | } else { 94 | if (children.isEmpty) return 'JsonMap(children: [])'; 95 | return 'JsonMap(children: [\n' 96 | '${children.map((e) => '${_indentString('${e.value}')}\n').join()}' 97 | '])'; 98 | } 99 | } 100 | 101 | @override 102 | String toJsonString() { 103 | throw UnimplementedError(); 104 | } 105 | } 106 | 107 | class JsonArray extends JsonElement { 108 | JsonArray({required this.children, required this.space}); 109 | 110 | @override 111 | List> children; 112 | String space; 113 | 114 | Token operator [](int index) { 115 | return children[index]; 116 | } 117 | 118 | @override 119 | Object? toJson() { 120 | return [for (final child in children) child.value.toJson()]; 121 | } 122 | 123 | @override 124 | String toString() { 125 | if (space.isNotEmpty) { 126 | if (children.isEmpty) { 127 | return 'JsonArray(children: [], space: ${jsonEncode(space)})'; 128 | } 129 | return 'JsonArray(\n' 130 | ' children: [\n' 131 | ' ${children.map((e) => '${_indentString('${e.value}', false, 2)}\n').join()}' 132 | ' ],\n' 133 | ' space: ${jsonEncode(space)}\n' 134 | ')'; 135 | } else { 136 | if (children.isEmpty) return 'JsonArray(children: [])'; 137 | return 'JsonArray(children: [\n' 138 | '${children.map((e) => '${_indentString('${e.value}')}\n').join()}' 139 | '])'; 140 | } 141 | } 142 | 143 | @override 144 | String toJsonString() { 145 | return '{${children.map((e) => e.value.toJsonString()).join()}$space}'; 146 | } 147 | } 148 | 149 | class JsonMapEntry extends JsonElement { 150 | JsonMapEntry({ 151 | required this.beforeKey, 152 | required this.key, 153 | required this.afterKey, 154 | required this.value, 155 | }); 156 | 157 | String beforeKey; 158 | Token key; 159 | String afterKey; 160 | Token value; 161 | 162 | @override 163 | Iterable> get children => [value]; 164 | 165 | @override 166 | MapEntry toMapEntry() { 167 | return MapEntry(key.value, value.value.toJson()); 168 | } 169 | 170 | @override 171 | Object? toJson() => value.value.toJson(); 172 | 173 | @override 174 | String toString() { 175 | return 'JsonMapEntry(\n' 176 | ' beforeKey: ${jsonEncode(beforeKey)}\n' 177 | ' key: ${jsonEncode(key.value)}\n' 178 | ' afterKey: ${jsonEncode(afterKey)}\n' 179 | ' value: ${_indentString('${value.value}', true)}\n' 180 | ')'; 181 | } 182 | 183 | @override 184 | String toJsonString() { 185 | return '$beforeKey${key.input}$afterKey:${value.value.toJsonString()}'; 186 | } 187 | } 188 | 189 | class JsonLiteral extends JsonElement { 190 | JsonLiteral({required this.value}); 191 | 192 | Token value; 193 | 194 | @override 195 | Object? toJson() => value.value; 196 | 197 | @override 198 | String toString() { 199 | return 'JsonLiteral(value: ${jsonEncode(value.value)})'; 200 | } 201 | 202 | @override 203 | Iterable> get children => const []; 204 | 205 | @override 206 | String toJsonString() => value.input; 207 | } 208 | -------------------------------------------------------------------------------- /puro/lib/src/commands/puro_install.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file/file.dart'; 4 | 5 | import '../../models.dart'; 6 | import '../command.dart'; 7 | import '../command_result.dart'; 8 | import '../config.dart'; 9 | import '../env/env_shims.dart'; 10 | import '../install/bin.dart'; 11 | import '../install/profile.dart'; 12 | import '../logger.dart'; 13 | import '../version.dart'; 14 | 15 | class PuroInstallCommand extends PuroCommand { 16 | PuroInstallCommand() { 17 | argParser.addFlag( 18 | 'force', 19 | help: 'Overwrite an existing puro installation, if any', 20 | negatable: false, 21 | ); 22 | argParser.addFlag( 23 | 'promote', 24 | help: 'Promotes a standalone executable to a full installation', 25 | negatable: false, 26 | ); 27 | argParser.addFlag( 28 | 'path', 29 | help: 'Whether or not to update the PATH automatically', 30 | ); 31 | argParser.addOption( 32 | 'profile', 33 | help: 34 | 'Overrides the profile script puro appends to when updating the PATH', 35 | ); 36 | } 37 | 38 | @override 39 | final name = 'install-puro'; 40 | 41 | @override 42 | bool get hidden => true; 43 | 44 | @override 45 | final description = 'Finishes installation of the puro tool'; 46 | 47 | @override 48 | bool get allowUpdateCheck => false; 49 | 50 | @override 51 | Future run() async { 52 | final puroVersion = await PuroVersion.of(scope); 53 | final config = PuroConfig.of(scope); 54 | final log = PuroLogger.of(scope); 55 | 56 | final force = argResults!['force'] as bool; 57 | final promote = argResults!['promote'] as bool; 58 | final profileOverride = argResults!['profile'] as String?; 59 | final updatePath = argResults!.wasParsed('path') 60 | ? argResults!['path'] as bool 61 | : null; 62 | 63 | await ensurePuroInstalled(scope: scope, force: force, promote: promote); 64 | 65 | final PuroGlobalPrefsModel prefs = await updateGlobalPrefs( 66 | scope: scope, 67 | fn: (prefs) { 68 | if (profileOverride != null) prefs.profileOverride = profileOverride; 69 | if (updatePath != null) prefs.enableProfileUpdate = updatePath; 70 | if (runner.pubCacheOverride != null) { 71 | prefs.pubCacheDir = runner.pubCacheOverride!; 72 | } 73 | if (runner.flutterGitUrlOverride != null) { 74 | prefs.flutterGitUrl = runner.flutterGitUrlOverride!; 75 | } 76 | if (runner.engineGitUrlOverride != null) { 77 | prefs.engineGitUrl = runner.engineGitUrlOverride!; 78 | } 79 | if (runner.dartSdkGitUrlOverride != null) { 80 | prefs.dartSdkGitUrl = runner.dartSdkGitUrlOverride!; 81 | } 82 | if (runner.versionsJsonUrlOverride != null) { 83 | prefs.releasesJsonUrl = runner.versionsJsonUrlOverride!; 84 | } 85 | if (runner.flutterStorageBaseUrlOverride != null) { 86 | prefs.flutterStorageBaseUrl = runner.flutterStorageBaseUrlOverride!; 87 | } 88 | if (runner.shouldInstallOverride != null) { 89 | prefs.shouldInstall = runner.shouldInstallOverride!; 90 | } 91 | if (runner.legacyPubCache != null) { 92 | prefs.legacyPubCache = runner.legacyPubCache!; 93 | } 94 | }, 95 | ); 96 | 97 | log.d(() => 'prefs: ${prettyJsonEncoder.convert(prefs.toProto3Json())}'); 98 | 99 | // Update the PATH by default if this is a distribution install. 100 | String? profilePath; 101 | var updatedWindowsRegistry = false; 102 | final homeDir = config.homeDir.path; 103 | if ((updatePath ?? false) || 104 | ((puroVersion.type == PuroInstallationType.distribution || promote) && 105 | !prefs.hasEnableProfileUpdate() || 106 | prefs.enableProfileUpdate)) { 107 | if (Platform.isLinux || Platform.isMacOS) { 108 | final profile = await installProfileEnv( 109 | scope: scope, 110 | profileOverride: prefs.hasProfileOverride() 111 | ? prefs.profileOverride 112 | : null, 113 | ); 114 | profilePath = profile?.path; 115 | if (profilePath != null && profilePath.startsWith(homeDir)) { 116 | profilePath = '~' + profilePath.substring(homeDir.length); 117 | } 118 | } else if (Platform.isWindows) { 119 | updatedWindowsRegistry = await tryUpdateWindowsPath(scope: scope); 120 | } 121 | } 122 | 123 | // Environment shims may have changed, update all of them to be safe 124 | config.envsDir.createSync(recursive: true); 125 | for (final envDir in config.envsDir.listSync().whereType()) { 126 | if (envDir.basename == 'default') continue; 127 | final environment = config.getEnv(envDir.basename); 128 | if (!environment.flutterDir.childDirectory('.git').existsSync()) continue; 129 | await runOptional(scope, '`${environment.name}` post-upgrade', () async { 130 | await installEnvShims(scope: scope, environment: environment); 131 | }); 132 | } 133 | 134 | final externalMessage = await detectExternalFlutterInstallations( 135 | scope: scope, 136 | ); 137 | 138 | final updateMessage = await checkIfUpdateAvailable( 139 | scope: scope, 140 | runner: runner, 141 | alwaysNotify: true, 142 | ); 143 | 144 | return BasicMessageResult.list([ 145 | if (externalMessage != null) externalMessage, 146 | if (updateMessage != null) updateMessage, 147 | if (profilePath != null) 148 | CommandMessage( 149 | 'Updated PATH in $profilePath, reopen your terminal or `source $profilePath` for it to take effect', 150 | ), 151 | if (updatedWindowsRegistry) 152 | CommandMessage( 153 | 'Updated PATH in the Windows registry, reopen your terminal for it to take effect', 154 | ), 155 | CommandMessage( 156 | 'Successfully installed Puro ${puroVersion.semver} to `${config.puroRoot.path}`', 157 | ), 158 | ]); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /puro/lib/src/env/releases.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:clock/clock.dart'; 5 | import 'package:pub_semver/pub_semver.dart'; 6 | 7 | import '../command_result.dart'; 8 | import '../config.dart'; 9 | import '../extensions.dart'; 10 | import '../file_lock.dart'; 11 | import '../git.dart'; 12 | import '../http.dart'; 13 | import '../logger.dart'; 14 | import '../progress.dart'; 15 | import '../proto/flutter_releases.pb.dart'; 16 | import '../provider.dart'; 17 | import 'version.dart'; 18 | 19 | /// Fetches all of the available Flutter releases. 20 | Future fetchFlutterReleases({ 21 | required Scope scope, 22 | }) async { 23 | return ProgressNode.of(scope).wrap((scope, node) async { 24 | final client = scope.read(clientProvider); 25 | final config = PuroConfig.of(scope); 26 | node.description = 'Fetching ${config.releasesJsonUrl}'; 27 | final response = await client.get(config.releasesJsonUrl); 28 | HttpException.ensureSuccess(response); 29 | config.cachedReleasesJsonFile.parent.createSync(recursive: true); 30 | await writeBytesAtomic( 31 | scope: scope, 32 | bytes: response.bodyBytes, 33 | file: config.cachedReleasesJsonFile, 34 | ); 35 | return FlutterReleasesModel.create() 36 | ..mergeFromProto3Json(jsonDecode(response.body)); 37 | }); 38 | } 39 | 40 | Future getCachedFlutterReleases({ 41 | required Scope scope, 42 | bool unlessStale = false, 43 | }) async { 44 | final config = PuroConfig.of(scope); 45 | final stat = config.cachedReleasesJsonFile.statSync(); 46 | if (stat.type == FileSystemEntityType.notFound || 47 | (unlessStale && 48 | clock.now().difference(stat.modified) > const Duration(hours: 1))) { 49 | return null; 50 | } 51 | final content = await readAtomic( 52 | scope: scope, 53 | file: config.cachedReleasesJsonFile, 54 | ); 55 | return FlutterReleasesModel.create() 56 | ..mergeFromProto3Json(jsonDecode(content)); 57 | } 58 | 59 | /// Searches [releases] for a specific version and/or channel. 60 | FlutterReleaseModel? searchFlutterVersions({ 61 | required FlutterReleasesModel releases, 62 | Version? version, 63 | FlutterChannel? channel, 64 | }) { 65 | if (version == null) { 66 | final hash = releases.currentRelease[channel?.name ?? 'stable']; 67 | if (hash == null) return null; 68 | return releases.releases.firstWhere((r) => r.hash == hash); 69 | } 70 | 71 | FlutterReleaseModel? result; 72 | FlutterChannel? resultChannel; 73 | final versionString = '$version'; 74 | for (final release in releases.releases) { 75 | if (release.version == versionString || 76 | (release.version.startsWith('v') && 77 | release.version.substring(1) == versionString)) { 78 | final releaseChannel = FlutterChannel.parse(release.channel)!; 79 | if (channel == releaseChannel) return release; 80 | if (result == null || releaseChannel.index < resultChannel!.index) { 81 | result = release; 82 | resultChannel = releaseChannel; 83 | } 84 | } 85 | } 86 | return result; 87 | } 88 | 89 | /// Finds a framework release matching [version] and/or [channel], pulling from 90 | /// a cache when possible. 91 | Future findFrameworkRelease({ 92 | required Scope scope, 93 | Version? version, 94 | FlutterChannel? channel, 95 | }) async { 96 | final config = PuroConfig.of(scope); 97 | final log = PuroLogger.of(scope); 98 | 99 | // Default to the stable channel 100 | if (channel == null && version == null) { 101 | channel = FlutterChannel.stable; 102 | } 103 | 104 | final cachedReleasesStat = config.cachedReleasesJsonFile.statSync(); 105 | final hasCache = cachedReleasesStat.type == FileSystemEntityType.file; 106 | var cacheIsFresh = 107 | hasCache && 108 | clock.now().difference(cachedReleasesStat.modified).inHours < 1; 109 | final isChannelOnly = channel != null && version == null; 110 | 111 | // Don't fetch from the cache if it's stale and we are looking for the latest 112 | // release. 113 | if (hasCache && (!isChannelOnly || cacheIsFresh)) { 114 | FlutterReleasesModel? cachedReleases; 115 | await lockFile(scope, config.cachedReleasesJsonFile, (handle) async { 116 | final contents = await handle.readAllAsString(); 117 | try { 118 | cachedReleases = FlutterReleasesModel.create() 119 | ..mergeFromProto3Json(jsonDecode(contents)); 120 | } catch (exception, stackTrace) { 121 | log.w('Error while parsing cached releases'); 122 | log.w('$exception\n$stackTrace'); 123 | cacheIsFresh = false; 124 | } 125 | }); 126 | if (cachedReleases != null) { 127 | final foundRelease = searchFlutterVersions( 128 | releases: cachedReleases!, 129 | version: version, 130 | channel: channel, 131 | ); 132 | if (foundRelease != null) return foundRelease; 133 | } 134 | } 135 | 136 | // Fetch new releases as long as the cache isn't already fresh. 137 | if (!cacheIsFresh) { 138 | final foundRelease = searchFlutterVersions( 139 | releases: await fetchFlutterReleases(scope: scope), 140 | version: version, 141 | channel: channel, 142 | ); 143 | if (foundRelease != null) return foundRelease; 144 | } 145 | 146 | if (version == null) { 147 | channel ??= FlutterChannel.stable; 148 | throw CommandError( 149 | 'Could not find latest version of the ${channel.name} channel', 150 | ); 151 | } else if (channel == null) { 152 | return null; 153 | } else { 154 | throw CommandError( 155 | 'Could not find version $version in the $channel channel', 156 | ); 157 | } 158 | } 159 | 160 | /// Attempts to find the current flutter channel. 161 | Future getFlutterChannel({ 162 | required Scope scope, 163 | required FlutterConfig config, 164 | }) async { 165 | final git = GitClient.of(scope); 166 | final branch = await git.getBranch(repository: config.sdkDir); 167 | if (branch == null) return null; 168 | return FlutterChannel.parse(branch); 169 | } 170 | 171 | const pseudoEnvironmentNames = {'stable', 'beta', 'master'}; 172 | -------------------------------------------------------------------------------- /puro/lib/src/workspace/vscode.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | 3 | import '../config.dart'; 4 | import '../extensions.dart'; 5 | import '../json_edit/editor.dart'; 6 | import '../logger.dart'; 7 | import '../process.dart'; 8 | import '../provider.dart'; 9 | import 'common.dart'; 10 | 11 | class VSCodeConfig extends IdeConfig { 12 | VSCodeConfig({ 13 | required super.workspaceDir, 14 | required super.projectConfig, 15 | super.flutterSdkDir, 16 | super.dartSdkDir, 17 | required super.exists, 18 | }); 19 | 20 | late final configDir = workspaceDir.childDirectory('.vscode'); 21 | late final settingsFile = configDir.childFile('settings.json'); 22 | 23 | @override 24 | String get name => 'VSCode'; 25 | 26 | JsonEditor readSettings() { 27 | if (!settingsFile.existsSync()) { 28 | return JsonEditor(source: '{}', indentLevel: 4); 29 | } 30 | final source = settingsFile.readAsStringSync(); 31 | return JsonEditor(source: source.isEmpty ? '{}' : source, indentLevel: 4); 32 | } 33 | 34 | static const flutterSdkDirKey = 'dart.flutterSdkPath'; 35 | static const dartSdkDirKey = 'dart.sdkPath'; 36 | 37 | @override 38 | Future backup({required Scope scope}) async { 39 | final config = PuroConfig.of(scope); 40 | final dotfile = projectConfig.readDotfileForWriting(); 41 | var changedDotfile = false; 42 | if (flutterSdkDir != null && 43 | !flutterSdkDir!.parent.parent.pathEquals(config.envsDir) && 44 | !dotfile.hasPreviousFlutterSdk()) { 45 | dotfile.previousFlutterSdk = flutterSdkDir!.path; 46 | changedDotfile = true; 47 | } 48 | if (dartSdkDir != null && 49 | dartSdkDir!.existsSync() && 50 | !dartSdkDir!.resolve().parent.parent.resolvedPathEquals( 51 | config.sharedCachesDir, 52 | ) && 53 | !dotfile.hasPreviousDartSdk()) { 54 | dotfile.previousDartSdk = dartSdkDir!.path; 55 | changedDotfile = true; 56 | } 57 | if (changedDotfile) { 58 | await projectConfig.writeDotfile(scope, dotfile); 59 | } 60 | } 61 | 62 | @override 63 | Future restore({required Scope scope}) async { 64 | final config = PuroConfig.of(scope); 65 | final dotfile = projectConfig.readDotfileForWriting(); 66 | var changedDotfile = false; 67 | if (dotfile.hasPreviousFlutterSdk()) { 68 | flutterSdkDir = config.fileSystem.directory(dotfile.previousFlutterSdk); 69 | dotfile.clearPreviousFlutterSdk(); 70 | changedDotfile = true; 71 | } else { 72 | flutterSdkDir = null; 73 | } 74 | if (dotfile.hasPreviousDartSdk()) { 75 | dartSdkDir = config.fileSystem.directory(dotfile.previousDartSdk); 76 | dotfile.clearPreviousDartSdk(); 77 | changedDotfile = true; 78 | } else { 79 | dartSdkDir = null; 80 | } 81 | if (changedDotfile) { 82 | await projectConfig.writeDotfile(scope, dotfile); 83 | } 84 | return save(scope: scope); 85 | } 86 | 87 | @override 88 | Future save({required Scope scope}) async { 89 | final log = PuroLogger.of(scope); 90 | final editor = readSettings(); 91 | 92 | if (flutterSdkDir == null) { 93 | editor.remove([flutterSdkDirKey]); 94 | } else { 95 | editor.update([flutterSdkDirKey], flutterSdkDir?.path); 96 | } 97 | 98 | if (dartSdkDir == null) { 99 | editor.remove([dartSdkDirKey]); 100 | } else { 101 | editor.update([dartSdkDirKey], dartSdkDir?.path); 102 | } 103 | 104 | if (editor.query([flutterSdkDirKey])?.value.toJson() != 105 | flutterSdkDir?.path || 106 | editor.query([dartSdkDirKey])?.value.toJson() != dartSdkDir?.path) { 107 | throw AssertionError('Corrupt settings.json'); 108 | } 109 | 110 | // Delete settings.json and .vscode if they are empty 111 | if (editor.source.trim() == '{}' && settingsFile.existsSync()) { 112 | settingsFile.deleteSync(); 113 | if (configDir.listSync().isEmpty) { 114 | configDir.deleteSync(); 115 | } 116 | } 117 | 118 | log.v('Writing to `${settingsFile.path}`'); 119 | settingsFile.parent.createSync(recursive: true); 120 | settingsFile.writeAsStringSync(editor.source); 121 | } 122 | 123 | static Future load({ 124 | required Scope scope, 125 | required Directory projectDir, 126 | required ProjectConfig projectConfig, 127 | }) async { 128 | final log = PuroLogger.of(scope); 129 | final config = PuroConfig.of(scope); 130 | final workspaceDir = config.findVSCodeWorkspaceDir(projectDir); 131 | log.v('vscode workspaceDir: $workspaceDir'); 132 | if (workspaceDir == null) { 133 | return VSCodeConfig( 134 | workspaceDir: 135 | findProjectDir(projectDir, '.idea') ?? 136 | projectConfig.ensureParentProjectDir(), 137 | projectConfig: projectConfig, 138 | exists: false, 139 | ); 140 | } 141 | final vscodeConfig = VSCodeConfig( 142 | workspaceDir: workspaceDir, 143 | projectConfig: projectConfig, 144 | exists: true, 145 | ); 146 | if (vscodeConfig.settingsFile.existsSync() && 147 | vscodeConfig.settingsFile.lengthSync() > 0) { 148 | final editor = vscodeConfig.readSettings(); 149 | final flutterSdkPathStr = editor 150 | .query([flutterSdkDirKey]) 151 | ?.value 152 | .toJson(); 153 | if (flutterSdkPathStr is String) { 154 | vscodeConfig.flutterSdkDir = config.fileSystem.directory( 155 | flutterSdkPathStr, 156 | ); 157 | } 158 | final dartSdkPathStr = editor.query([dartSdkDirKey])?.value.toJson(); 159 | if (dartSdkPathStr is String) { 160 | vscodeConfig.dartSdkDir = config.fileSystem.directory(dartSdkPathStr); 161 | } 162 | } 163 | return vscodeConfig; 164 | } 165 | } 166 | 167 | Future isRunningInVscode({required Scope scope}) async { 168 | final processes = await getParentProcesses(scope: scope); 169 | return processes.any( 170 | (e) => 171 | e.name == 'Code.exe' || 172 | e.name == 'VSCode.exe' || 173 | e.name == 'VSCodium.exe' || 174 | e.name == 'code' || 175 | e.name == 'codium' || 176 | e.name == 'Cursor' || 177 | e.name == 'cursor', 178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /puro/lib/src/command_result.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../models.dart'; 4 | import 'provider.dart'; 5 | import 'terminal.dart'; 6 | 7 | extension CommandResultModelExtensions on CommandResultModel { 8 | void addMessage(CommandMessage message, OutputFormatter format) { 9 | messages.add( 10 | CommandMessageModel( 11 | type: 12 | (message.type ?? 13 | (success ? CompletionType.success : CompletionType.failure)) 14 | .name, 15 | message: message.message(format), 16 | ), 17 | ); 18 | } 19 | 20 | void addMessages(Iterable messages, OutputFormatter format) { 21 | for (final message in messages) { 22 | addMessage(message, format); 23 | } 24 | } 25 | } 26 | 27 | class CommandErrorResult extends CommandResult { 28 | CommandErrorResult(this.exception, this.stackTrace, this.logLevel); 29 | 30 | final Object exception; 31 | final StackTrace stackTrace; 32 | final int? logLevel; 33 | 34 | @override 35 | Iterable get messages { 36 | return [ 37 | CommandMessage('$exception\n$stackTrace'), 38 | CommandMessage( 39 | [ 40 | 'Puro crashed! Please file an issue at https://github.com/pingbird/puro', 41 | if (logLevel != null && logLevel! < 4) 42 | 'Consider running the command with a higher log level: `--log-level=4`', 43 | ].join('\n'), 44 | ), 45 | ]; 46 | } 47 | 48 | @override 49 | bool get success => false; 50 | 51 | @override 52 | CommandResultModel? get model => CommandResultModel( 53 | error: CommandErrorModel( 54 | exception: '$exception', 55 | exceptionType: '${exception.runtimeType}', 56 | stackTrace: '$stackTrace', 57 | ), 58 | ); 59 | } 60 | 61 | class CommandHelpResult extends CommandResult { 62 | CommandHelpResult({required this.didRequestHelp, this.help, this.usage}); 63 | 64 | final bool didRequestHelp; 65 | final String? help; 66 | final String? usage; 67 | 68 | @override 69 | Iterable get messages => [ 70 | if (message != null) CommandMessage(help!, type: CompletionType.failure), 71 | if (usage != null) 72 | CommandMessage( 73 | usage!, 74 | type: message == null && didRequestHelp 75 | ? CompletionType.plain 76 | : CompletionType.info, 77 | ), 78 | ]; 79 | 80 | @override 81 | bool get success => didRequestHelp; 82 | 83 | @override 84 | CommandResultModel? get model => CommandResultModel(usage: usage); 85 | } 86 | 87 | class BasicMessageResult extends CommandResult { 88 | BasicMessageResult( 89 | String message, { 90 | this.success = true, 91 | CompletionType? type, 92 | this.model, 93 | }) : messages = [CommandMessage(message, type: type)]; 94 | 95 | BasicMessageResult.format( 96 | String Function(OutputFormatter format) message, { 97 | this.success = true, 98 | CompletionType? type, 99 | this.model, 100 | }) : messages = [CommandMessage.format(message, type: type)]; 101 | 102 | BasicMessageResult.list(this.messages, {this.success = true, this.model}); 103 | 104 | @override 105 | final bool success; 106 | @override 107 | final List messages; 108 | @override 109 | final CommandResultModel? model; 110 | } 111 | 112 | abstract class CommandResult { 113 | bool get success; 114 | 115 | CommandMessage? get message => null; 116 | 117 | Iterable get messages => [message!]; 118 | 119 | CommandResultModel? get model => null; 120 | 121 | CommandResultModel toModel([OutputFormatter format = plainFormatter]) { 122 | final result = CommandResultModel(); 123 | if (model != null) { 124 | result.mergeFromMessage(model!); 125 | } 126 | result.success = success; 127 | result.addMessages(messages, format); 128 | return result; 129 | } 130 | 131 | @override 132 | String toString() => CommandMessage.formatMessages( 133 | messages: messages, 134 | format: plainFormatter, 135 | success: toModel().success, 136 | ); 137 | } 138 | 139 | class CommandMessage { 140 | CommandMessage(String message, {this.type}) : message = ((format) => message); 141 | CommandMessage.format(this.message, {this.type}); 142 | 143 | final CompletionType? type; 144 | final String Function(OutputFormatter format) message; 145 | 146 | static String formatMessages({ 147 | required Iterable messages, 148 | required OutputFormatter format, 149 | required bool success, 150 | }) { 151 | return messages 152 | .map( 153 | (e) => format.complete( 154 | e.message(format), 155 | type: 156 | e.type ?? 157 | (success ? CompletionType.success : CompletionType.failure), 158 | ), 159 | ) 160 | .join('\n'); 161 | } 162 | 163 | static final provider = Provider( 164 | (scope) => (message) {}, 165 | ); 166 | 167 | void queue(Scope scope) => scope.read(provider)(this); 168 | } 169 | 170 | /// Like [CommandResult] but thrown as an exception. 171 | class CommandError implements Exception { 172 | CommandError( 173 | String message, { 174 | CompletionType? type, 175 | CommandResultModel? model, 176 | bool success = false, 177 | }) : result = BasicMessageResult( 178 | message, 179 | success: success, 180 | type: type, 181 | model: model, 182 | ); 183 | 184 | CommandError.format( 185 | String Function(OutputFormatter format) message, { 186 | CompletionType? type, 187 | CommandResultModel? model, 188 | bool success = false, 189 | }) : result = BasicMessageResult.format( 190 | message, 191 | success: success, 192 | type: type, 193 | model: model, 194 | ); 195 | 196 | CommandError.list( 197 | List messages, { 198 | CommandResultModel? model, 199 | bool success = false, 200 | }) : result = BasicMessageResult.list( 201 | messages, 202 | success: success, 203 | model: model, 204 | ); 205 | 206 | final CommandResult result; 207 | 208 | @override 209 | String toString() => result.toString(); 210 | } 211 | 212 | class UnsupportedOSError extends CommandError { 213 | UnsupportedOSError() : super('Unsupported OS: `${Platform.operatingSystem}`'); 214 | } 215 | -------------------------------------------------------------------------------- /puro/lib/src/progress.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:clock/clock.dart'; 4 | import 'package:http/http.dart'; 5 | import 'package:neoansi/neoansi.dart'; 6 | 7 | import 'git.dart'; 8 | import 'logger.dart'; 9 | import 'provider.dart'; 10 | import 'terminal.dart'; 11 | 12 | abstract class ProgressNode { 13 | ProgressNode({required this.scope}); 14 | 15 | final Scope scope; 16 | 17 | final stackTrace = StackTrace.current; 18 | 19 | late final terminal = Terminal.of(scope); 20 | 21 | void Function()? _onChanged; 22 | 23 | final children = []; 24 | 25 | void addNode(ActiveProgressNode node) { 26 | assert(!children.contains(node)); 27 | assert(node._onChanged == null); 28 | node._onChanged = () { 29 | if (_onChanged != null) _onChanged!(); 30 | }; 31 | children.add(node); 32 | if (_onChanged != null) _onChanged!(); 33 | } 34 | 35 | void removeNode(ActiveProgressNode node) { 36 | assert(children.contains(node)); 37 | node._onChanged = null; 38 | children.remove(node); 39 | if (_onChanged != null) _onChanged!(); 40 | } 41 | 42 | Future wrap( 43 | Future Function(Scope scope, ActiveProgressNode node) fn, { 44 | bool removeWhenComplete = true, 45 | bool optional = false, 46 | }) async { 47 | final start = clock.now(); 48 | final log = PuroLogger.of(scope); 49 | final node = ActiveProgressNode(scope: OverrideScope(parent: scope)); 50 | node.scope.add(ProgressNode.provider, node); 51 | scheduleMicrotask(() { 52 | addNode(node); 53 | }); 54 | try { 55 | if (optional) { 56 | try { 57 | return await fn(scope, node); 58 | } catch (exception, stackTrace) { 59 | if (node.description != null) { 60 | log.e('Exception while ${node.description}'); 61 | } 62 | log.e('$exception\n$stackTrace'); 63 | return null as T; 64 | } 65 | } else { 66 | return await fn(scope, node); 67 | } 68 | } finally { 69 | node.complete = true; 70 | if (removeWhenComplete) removeNode(node); 71 | log.v( 72 | '${node.description} took ${clock.now().difference(start).inMilliseconds}ms', 73 | ); 74 | } 75 | } 76 | 77 | String render(); 78 | 79 | static final provider = Provider((scope) { 80 | return RootProgressNode(scope: scope); 81 | }); 82 | static ProgressNode of(Scope scope) => scope.read(provider); 83 | } 84 | 85 | class ActiveProgressNode extends ProgressNode { 86 | ActiveProgressNode({required super.scope}); 87 | 88 | Timer? _delayTimer; 89 | String? _description; 90 | String? get description => _description; 91 | set description(String? description) { 92 | if (description != null) { 93 | PuroLogger.of(scope).v( 94 | 'Started ${description.substring(0, 1).toLowerCase()}' 95 | '${description.substring(1)}', 96 | ); 97 | } 98 | if (_description == description) return; 99 | _description = description; 100 | if (_onChanged != null) _onChanged!(); 101 | } 102 | 103 | num? _progress; 104 | num? get progress => _progress; 105 | set progress(num? progress) { 106 | if (_progress == progress) return; 107 | _progress = progress; 108 | if (_onChanged != null) _onChanged!(); 109 | } 110 | 111 | num? _progressTotal; 112 | num? get progressTotal => _progressTotal; 113 | set progressTotal(num? progressTotal) { 114 | if (_progressTotal == progressTotal) return; 115 | _progressTotal = progressTotal; 116 | if (_onChanged != null) _onChanged!(); 117 | } 118 | 119 | var _complete = false; 120 | bool get complete => _complete; 121 | set complete(bool complete) { 122 | if (_complete == complete) return; 123 | _complete = complete; 124 | if (_onChanged != null) _onChanged!(); 125 | } 126 | 127 | void delay(Duration duration) { 128 | _delayed = true; 129 | _delayTimer?.cancel(); 130 | _delayTimer = Timer(duration, () { 131 | _delayed = false; 132 | if (_onChanged != null) _onChanged!(); 133 | }); 134 | } 135 | 136 | var _delayed = false; 137 | bool get delayed => _delayed; 138 | 139 | bool get visible => !delayed; 140 | 141 | Stream> wrapHttpResponse(StreamedResponse response) { 142 | progressTotal = response.contentLength; 143 | progress = 0; 144 | return wrapByteStream(response.stream); 145 | } 146 | 147 | Stream> wrapByteStream(Stream> stream) { 148 | return stream.map((event) { 149 | progress = (progress ?? 0) + event.length; 150 | return event; 151 | }); 152 | } 153 | 154 | void onCloneProgress(GitCloneStep step, double progress) { 155 | progressTotal = GitCloneStep.values.length; 156 | this.progress = step.index + progress; 157 | } 158 | 159 | double? get progressFraction { 160 | if (_progress == null || _progressTotal == null) { 161 | return null; 162 | } 163 | return (_progress! / _progressTotal!).clamp(0.0, 1.0); 164 | } 165 | 166 | static String _indentString(String input, String indent) { 167 | return input.split('\n').map((e) => '$indent$e').join('\n'); 168 | } 169 | 170 | @override 171 | String render() { 172 | if (!visible) return ''; 173 | const width = 15; 174 | final progressFraction = this.progressFraction; 175 | String text; 176 | if (progressFraction == null) { 177 | text = '[${('/ ' * (width ~/ 2 + 1)).substring(0, width)}]'; 178 | } else { 179 | final progressChars = (progressFraction * width).round(); 180 | text = '[${('=' * progressChars).padRight(width)}]'; 181 | } 182 | text = terminal.format.color( 183 | text, 184 | foregroundColor: Ansi8BitColor.blue, 185 | bold: true, 186 | ); 187 | if (_description != null) { 188 | text = '$text $description'; 189 | } 190 | if (children.isNotEmpty) { 191 | text = 192 | '$text\n${_indentString('${children.where((e) => e.visible).map((e) => e.render()).join('\n')}', ' ')}'; 193 | } 194 | return text; 195 | } 196 | } 197 | 198 | class RootProgressNode extends ProgressNode { 199 | RootProgressNode({required super.scope}) { 200 | _onChanged = () { 201 | terminal.status = render(); 202 | }; 203 | } 204 | 205 | @override 206 | String render() { 207 | return children.map((e) => e.render()).join('\n'); 208 | } 209 | } 210 | --------------------------------------------------------------------------------