diff --git a/puro/CHANGELOG.md b/puro/CHANGELOG.md index f7b4c0c..6cfef39 100644 --- a/puro/CHANGELOG.md +++ b/puro/CHANGELOG.md @@ -1,3 +1,6 @@ +## Unreleased + +* Added the `prepare` command to pre-download Flutter artifacts for an environment ## 1.5.0 * Fixed several major bugs diff --git a/puro/lib/src/cli.dart b/puro/lib/src/cli.dart index f0c17eb..d673ee6 100644 --- a/puro/lib/src/cli.dart +++ b/puro/lib/src/cli.dart @@ -21,6 +21,7 @@ import 'commands/internal_generate_ast_parser.dart'; import 'commands/internal_generate_docs.dart'; import 'commands/ls_versions.dart'; import 'commands/prefs.dart'; +import 'commands/prepare.dart'; import 'commands/pub.dart'; import 'commands/puro_install.dart'; import 'commands/puro_uninstall.dart'; @@ -259,6 +260,7 @@ void main(List args) async { ..addCommand(CleanCommand()) ..addCommand(EnvRmCommand()) ..addCommand(EnvRenameCommand()) + ..addCommand(PrepareCommand()) ..addCommand(FlutterCommand()) ..addCommand(DartCommand()) ..addCommand(PubCommand()) diff --git a/puro/lib/src/commands/prepare.dart b/puro/lib/src/commands/prepare.dart new file mode 100644 index 0000000..0ddc33b --- /dev/null +++ b/puro/lib/src/commands/prepare.dart @@ -0,0 +1,84 @@ +import '../command.dart'; +import '../command_result.dart'; +import '../env/default.dart'; +import '../env/prepare.dart'; +import '../logger.dart'; + +class PrepareCommand extends PuroCommand { + PrepareCommand() { + argParser + ..addFlag( + 'force', + help: 'Force re-downloading artifacts even if they already exist.', + negatable: false, + ) + ..addFlag( + 'all-platforms', + help: 'Precache artifacts for every supported Flutter platform.', + negatable: false, + ) + ..addMultiOption( + 'platform', + help: + 'Precache artifacts for the provided platforms (android, ios, etc).', + valueHelp: 'name', + allowed: preparePlatformOptions.toList()..sort(), + ); + } + + @override + final name = 'prepare'; + + @override + final description = + 'Pre-downloads Flutter artifacts for an environment so builds can start immediately.'; + + @override + String? get argumentUsage => '[env]'; + + @override + Future run() async { + final log = PuroLogger.of(scope); + final envName = unwrapSingleOptionalArgument(); + final environment = await getProjectEnvOrDefault( + scope: scope, + envName: envName, + ); + + final force = argResults!['force'] as bool; + final allPlatforms = argResults!['all-platforms'] as bool; + final requestedPlatforms = (argResults!['platform'] as List).map( + (e) => e.toLowerCase(), + ); + final sortedRequested = sortPreparePlatforms(requestedPlatforms); + final defaultPlatforms = defaultPreparePlatforms(); + + final platforms = sortedRequested.isNotEmpty + ? sortedRequested + : (allPlatforms ? [] : defaultPlatforms); + + log.d( + 'Preparing environment `${environment.name}` for platforms: ' + '${allPlatforms ? 'all platforms' : (platforms.isEmpty ? 'default set' : platforms.join(', '))}' + '${force ? ' (force)' : ''}', + ); + + await prepareEnvironment( + scope: scope, + environment: environment, + platforms: platforms, + allPlatforms: allPlatforms, + force: force, + ); + + final platformSummary = allPlatforms + ? 'all platforms' + : (platforms.isEmpty + ? 'default platforms (${defaultPlatforms.join(', ')})' + : platforms.join(', ')); + + return BasicMessageResult( + 'Prepared environment `${environment.name}` ($platformSummary${force ? ', forced' : ''})', + ); + } +} diff --git a/puro/lib/src/env/prepare.dart b/puro/lib/src/env/prepare.dart new file mode 100644 index 0000000..f442a8c --- /dev/null +++ b/puro/lib/src/env/prepare.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +import '../command_result.dart'; +import '../config.dart'; +import '../provider.dart'; +import 'command.dart'; + +const Set preparePlatformOptions = { + 'android', + 'ios', + 'linux', + 'macos', + 'windows', + 'web', + 'fuchsia', +}; + +const List _platformOrder = [ + 'android', + 'ios', + 'macos', + 'linux', + 'windows', + 'web', + 'fuchsia', +]; + +List sortPreparePlatforms(Iterable platforms) { + final platformSet = platforms.toSet(); + return _platformOrder.where(platformSet.contains).toList(); +} + +List defaultPreparePlatforms() { + final platforms = {'android', 'web'}; + if (Platform.isMacOS) { + platforms + ..add('ios') + ..add('macos'); + } + if (Platform.isLinux) { + platforms.add('linux'); + } + if (Platform.isWindows) { + platforms.add('windows'); + } + return sortPreparePlatforms(platforms); +} + +Future prepareEnvironment({ + required Scope scope, + required EnvConfig environment, + List? platforms, + bool allPlatforms = false, + bool force = false, +}) async { + final effectivePlatforms = platforms == null + ? const [] + : sortPreparePlatforms(platforms); + final args = ['precache']; + if (force) { + args.add('--force'); + } + if (allPlatforms) { + args.add('--all-platforms'); + } + for (final platform in effectivePlatforms) { + args.add('--$platform'); + } + final exitCode = await runFlutterCommand( + scope: scope, + environment: environment, + args: args, + mode: ProcessStartMode.inheritStdio, + ); + if (exitCode != 0) { + throw CommandError( + '`flutter ${args.join(' ')}` failed with exit code $exitCode', + ); + } +} diff --git a/website/docs/reference/manual.md b/website/docs/reference/manual.md index 09a7f0d..510d05c 100644 --- a/website/docs/reference/manual.md +++ b/website/docs/reference/manual.md @@ -193,3 +193,18 @@ We can manually delete unused caches with the `puro gc` command: $> puro gc [✓] Cleaned up caches and reclaimed 2.7GB ``` + +### Preparing artifacts + +Use `puro prepare` to download Flutter artifacts ahead of time so the first build on a new +or freshly upgraded environment does not need to fetch them on demand: + +``` +$> puro prepare master +[✓] Prepared environment `master` (default platforms (android, ios, macos, web)) +``` + +Add `--all-platforms` to cache everything Flutter supports, or pass `--platform` multiple +times (for example `--platform android --platform web`) to tailor the download to the +projects you build most often. Include `--force` if you want to refresh artifacts even +when they are already present.