[flutter_migrate] Start command and executables (#2735)

* [flutter_migrate] Start command

* Sync with upstream changes

* Integrate with compute and add E2E tests

* E2E tests passing

* Resolve hangs, add help messages

* Use CommandRunner

* Cleanup hooks to prevent hang

* Formatting

* Softer requirements on test matching to ignore order

* Null safety, address minor comments

* Address comments

* Cleanup and logging improvements

* format, normalize

* Kick tests

* Fix tests

* Improve win test robustness

* Windows perms copyall

* Fix directory typo bug

* takeown command in CI

* Add logging for windows

* windows command debug logging

* Skip running on unsupported flutter versions

* Add main.dart

* Bots memory, dynamic flutter executable

* Different temp dir

* Takeown of main.dart

* test adjustments

* Analyzer

* Increase resources to dart_unit_tests

* Remove logging
This commit is contained in:
Gary Qian
2022-12-27 13:45:05 -05:00
committed by GitHub
parent a978884712
commit e425eea6c7
15 changed files with 1110 additions and 50 deletions

View File

@ -128,19 +128,6 @@ task:
- else
- echo "Only run in presubmit"
- fi
- name: dart_unit_tests
env:
matrix:
CHANNEL: "master"
CHANNEL: "stable"
unit_test_script:
- ./script/tool_runner.sh test --exclude=script/configs/dart_unit_tests_exceptions.yaml
pathified_unit_test_script:
# Run tests with path-based dependencies to ensure that publishing
# the changes won't break tests of other packages in the repository
# that depend on it.
- ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates
- $PLUGIN_TOOL_COMMAND test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml
- name: analyze
env:
matrix:
@ -256,7 +243,7 @@ task:
zone: us-central1-a
namespace: default
cpu: 4
memory: 12G
memory: 16G
matrix:
### Android tasks ###
- name: android-platform_tests
@ -320,6 +307,19 @@ task:
- cd ../..
- flutter packages get
- dart testing/web_benchmarks_test.dart
- name: dart_unit_tests
env:
matrix:
CHANNEL: "master"
CHANNEL: "stable"
unit_test_script:
- ./script/tool_runner.sh test --exclude=script/configs/dart_unit_tests_exceptions.yaml
pathified_unit_test_script:
# Run tests with path-based dependencies to ensure that publishing
# the changes won't break tests of other packages in the repository
# that depend on it.
- ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates
- $PLUGIN_TOOL_COMMAND test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml
# ARM macOS tasks.
task:

View File

@ -24,7 +24,11 @@ any migrations that are broken.
## Usage
The core command sequence to use is `start`, `apply`.
To run the tool enter the root directory of your flutter project and run:
`dart run <path_to_flutter_migrate_package>/bin/flutter_migrate.dart <subcommand> [parameters]`
The core subcommand sequence to use is `start`, `apply`.
* `start` will generate a migration that will be staged in the `migration_staging_directory`
in your project home. This command may take some time to complete depending on network speed.

View File

@ -0,0 +1,9 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_migrate/executable.dart' as executable;
void main(List<String> args) {
executable.main(args);
}

View File

@ -0,0 +1,95 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'src/base/command.dart';
import 'src/base/file_system.dart';
import 'src/base_dependencies.dart';
import 'src/commands/abandon.dart';
import 'src/commands/apply.dart';
import 'src/commands/start.dart';
import 'src/commands/status.dart';
Future<void> main(List<String> args) async {
final bool veryVerbose = args.contains('-vv');
final bool verbose =
args.contains('-v') || args.contains('--verbose') || veryVerbose;
final MigrateBaseDependencies baseDependencies = MigrateBaseDependencies();
final List<MigrateCommand> commands = <MigrateCommand>[
MigrateStartCommand(
verbose: verbose,
logger: baseDependencies.logger,
fileSystem: baseDependencies.fileSystem,
processManager: baseDependencies.processManager,
),
MigrateStatusCommand(
verbose: verbose,
logger: baseDependencies.logger,
fileSystem: baseDependencies.fileSystem,
processManager: baseDependencies.processManager,
),
MigrateAbandonCommand(
logger: baseDependencies.logger,
fileSystem: baseDependencies.fileSystem,
terminal: baseDependencies.terminal,
processManager: baseDependencies.processManager),
MigrateApplyCommand(
verbose: verbose,
logger: baseDependencies.logger,
fileSystem: baseDependencies.fileSystem,
terminal: baseDependencies.terminal,
processManager: baseDependencies.processManager),
];
final MigrateCommandRunner runner = MigrateCommandRunner();
commands.forEach(runner.addCommand);
await runner.run(args);
await _exit(0, baseDependencies,
shutdownHooks: baseDependencies.fileSystem.shutdownHooks);
await baseDependencies.fileSystem.dispose();
}
/// Simple extension of a CommandRunner to provide migrate specific global flags.
class MigrateCommandRunner extends CommandRunner<void> {
MigrateCommandRunner()
: super(
'flutter',
'Migrates legacy flutter projects to modern versions.',
) {
argParser.addFlag('verbose',
abbr: 'v',
negatable: false,
help: 'Noisy logging, including all shell commands executed.');
}
@override
ArgParser get argParser => _argParser;
final ArgParser _argParser = ArgParser();
}
Future<int> _exit(int code, MigrateBaseDependencies baseDependencies,
{required ShutdownHooks shutdownHooks}) async {
// Run shutdown hooks before flushing logs
await shutdownHooks.runShutdownHooks(baseDependencies.logger);
final Completer<void> completer = Completer<void>();
// Give the task / timer queue one cycle through before we hard exit.
Timer.run(() {
io.exit(code);
});
await completer.future;
return code;
}

View File

@ -4,6 +4,9 @@
import 'package:args/command_runner.dart';
import 'logger.dart';
import 'project.dart';
enum ExitStatus {
success,
warning,
@ -11,6 +14,9 @@ enum ExitStatus {
killed,
}
const String flutterNoPubspecMessage = 'Error: No pubspec.yaml file found.\n'
'This command should be run from the root of your Flutter project.';
class CommandResult {
const CommandResult(this.exitStatus);
@ -50,6 +56,7 @@ abstract class MigrateCommand extends Command<void> {
@override
Future<void> run() async {
await runCommand();
return;
}
Future<CommandResult> runCommand();
@ -71,4 +78,12 @@ abstract class MigrateCommand extends Command<void> {
/// Gets the parsed command-line option named [name] as an `int`.
int? intArg(String name) => argResults?[name] as int?;
bool validateWorkingDirectory(FlutterProject project, Logger logger) {
if (!project.pubspecFile.existsSync()) {
logger.printError(flutterNoPubspecMessage);
return false;
}
return true;
}
}

View File

@ -0,0 +1,87 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:process/process.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/signals.dart';
import 'base/terminal.dart';
/// Initializes the boilerplate dependencies needed by the migrate tool.
class MigrateBaseDependencies {
MigrateBaseDependencies() {
processManager = const LocalProcessManager();
fileSystem = LocalFileSystem(
LocalSignals.instance, Signals.defaultExitSignals, ShutdownHooks());
stdio = Stdio();
terminal = AnsiTerminal(stdio: stdio);
final LoggerFactory loggerFactory = LoggerFactory(
outputPreferences: OutputPreferences(
wrapText: stdio.hasTerminal,
showColor: stdout.supportsAnsiEscapes,
stdio: stdio,
),
terminal: terminal,
stdio: stdio,
);
logger = loggerFactory.createLogger(
windows: isWindows,
);
}
late final ProcessManager processManager;
late final LocalFileSystem fileSystem;
late final Stdio stdio;
late final Terminal terminal;
late final Logger logger;
}
/// An abstraction for instantiation of the correct logger type.
///
/// Our logger class hierarchy and runtime requirements are overly complicated.
class LoggerFactory {
LoggerFactory({
required Terminal terminal,
required Stdio stdio,
required OutputPreferences outputPreferences,
StopwatchFactory stopwatchFactory = const StopwatchFactory(),
}) : _terminal = terminal,
_stdio = stdio,
_stopwatchFactory = stopwatchFactory,
_outputPreferences = outputPreferences;
final Terminal _terminal;
final Stdio _stdio;
final StopwatchFactory _stopwatchFactory;
final OutputPreferences _outputPreferences;
/// Create the appropriate logger for the current platform and configuration.
Logger createLogger({
required bool windows,
}) {
Logger logger;
if (windows) {
logger = WindowsStdoutLogger(
terminal: _terminal,
stdio: _stdio,
outputPreferences: _outputPreferences,
stopwatchFactory: _stopwatchFactory,
);
} else {
logger = StdoutLogger(
terminal: _terminal,
stdio: _stdio,
outputPreferences: _outputPreferences,
stopwatchFactory: _stopwatchFactory);
}
return logger;
}
}

View File

@ -19,6 +19,7 @@ class MigrateAbandonCommand extends MigrateCommand {
required this.fileSystem,
required this.terminal,
required ProcessManager processManager,
this.standalone = false,
}) : migrateUtils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
@ -60,6 +61,8 @@ class MigrateAbandonCommand extends MigrateCommand {
final MigrateUtils migrateUtils;
final bool standalone;
@override
final String name = 'abandon';
@ -75,7 +78,12 @@ class MigrateAbandonCommand extends MigrateCommand {
? FlutterProject.current(fileSystem)
: flutterProjectFactory
.fromDirectory(fileSystem.directory(projectDirectory));
final bool isSubcommand = boolArg('flutter-subcommand') ?? false;
final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone;
if (!validateWorkingDirectory(project, logger)) {
return const CommandResult(ExitStatus.fail);
}
Directory stagingDirectory =
project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
final String? customStagingDirectoryPath = stringArg('staging-directory');

View File

@ -25,6 +25,7 @@ class MigrateApplyCommand extends MigrateCommand {
required this.fileSystem,
required this.terminal,
required ProcessManager processManager,
this.standalone = false,
}) : _verbose = verbose,
_processManager = processManager,
migrateUtils = MigrateUtils(
@ -76,6 +77,8 @@ class MigrateApplyCommand extends MigrateCommand {
final MigrateUtils migrateUtils;
final bool standalone;
@override
final String name = 'apply';
@ -98,12 +101,16 @@ class MigrateApplyCommand extends MigrateCommand {
final FlutterToolsEnvironment environment =
await FlutterToolsEnvironment.initializeFlutterToolsEnvironment(
_processManager, logger);
final bool isSubcommand = boolArg('flutter-subcommand') ?? false;
final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone;
if (!validateWorkingDirectory(project, logger)) {
return CommandResult.fail();
}
if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) {
logger.printStatus('No git repo found. Please run in a project with an '
'initialized git repo or initialize one with:');
printCommandText('git init', logger, standalone: null);
printCommand('git init', logger);
return const CommandResult(ExitStatus.fail);
}

View File

@ -0,0 +1,345 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:process/process.dart';
import '../base/command.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/project.dart';
import '../compute.dart';
import '../environment.dart';
import '../manifest.dart';
import '../result.dart';
import '../utils.dart';
class MigrateStartCommand extends MigrateCommand {
MigrateStartCommand({
bool verbose = false,
required this.logger,
required this.fileSystem,
required this.processManager,
this.standalone = false,
}) : _verbose = verbose,
migrateUtils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
processManager: processManager,
) {
argParser.addOption(
'staging-directory',
help:
'Specifies the custom migration staging directory used to stage and edit proposed changes. '
'This path can be absolute or relative to the flutter project root.',
valueHelp: 'path',
);
argParser.addOption(
'project-directory',
help: 'The root directory of the flutter project.',
valueHelp: 'path',
);
argParser.addOption(
'platforms',
help:
'Restrict the tool to only migrate the listed platforms. By default all platforms generated by '
'flutter create will be migrated. To indicate the project root, use the `root` platform',
valueHelp: 'root,android,ios,windows...',
);
argParser.addFlag(
'delete-temp-directories',
help:
'Indicates if the temporary directories created by the migrate tool will be deleted.',
);
argParser.addOption(
'base-app-directory',
help:
'The directory containing the base reference app. This is used as the common ancestor in a 3 way merge. '
'Providing this directory will prevent the tool from generating its own. This is primarily used '
'in testing and CI.',
valueHelp: 'path',
hide: !verbose,
);
argParser.addOption(
'target-app-directory',
help:
'The directory containing the target reference app. This is used as the target app in 3 way merge. '
'Providing this directory will prevent the tool from generating its own. This is primarily used '
'in testing and CI.',
valueHelp: 'path',
hide: !verbose,
);
argParser.addFlag(
'allow-fallback-base-revision',
help:
'If a base revision cannot be determined, this flag enables using flutter 1.0.0 as a fallback base revision. '
'Using this fallback will typically produce worse quality migrations and possibly more conflicts.',
);
argParser.addOption(
'base-revision',
help:
'Manually sets the base revision to generate the base ancestor reference app with. This may be used '
'if the tool is unable to determine an appropriate base revision.',
valueHelp: 'git revision hash',
);
argParser.addOption(
'target-revision',
help:
'Manually sets the target revision to generate the target reference app with. Passing this indicates '
'that the current flutter sdk version is not the version that should be migrated to.',
valueHelp: 'git revision hash',
);
argParser.addFlag(
'prefer-two-way-merge',
negatable: false,
help:
'Avoid three way merges when possible. Enabling this effectively ignores the base ancestor reference '
'files when a merge is required, opting for a simpler two way merge instead. In some edge cases typically '
'involving using a fallback or incorrect base revision, the default three way merge algorithm may produce '
'incorrect merges. Two way merges are more conflict prone, but less likely to produce incorrect results '
'silently.',
);
argParser.addFlag(
'flutter-subcommand',
help:
'Enable when using the flutter tool as a subcommand. This changes the '
'wording of log messages to indicate the correct suggested commands to use.',
);
}
final bool _verbose;
final Logger logger;
final FileSystem fileSystem;
final MigrateUtils migrateUtils;
final ProcessManager processManager;
final bool standalone;
@override
final String name = 'start';
@override
final String description =
r'Begins a new migration. Computes the changes needed to migrate the project from the base revision of Flutter to the current revision of Flutter and outputs the results in a working directory. Use `$ flutter migrate apply` accept and apply the changes.';
@override
Future<CommandResult> runCommand() async {
final FlutterToolsEnvironment environment =
await FlutterToolsEnvironment.initializeFlutterToolsEnvironment(
processManager, logger);
if (!_validateEnvironment(environment)) {
return const CommandResult(ExitStatus.fail);
}
final String? projectRootDirPath = stringArg('project-directory') ??
environment.getString('FlutterProject.directory');
final Directory projectRootDir = fileSystem.directory(projectRootDirPath);
final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory();
final FlutterProject project = projectRootDirPath == null
? FlutterProject.current(fileSystem)
: flutterProjectFactory
.fromDirectory(fileSystem.directory(projectRootDirPath));
if (!validateWorkingDirectory(project, logger)) {
return CommandResult.fail();
}
final bool isModule =
environment.getBool('FlutterProject.isModule') ?? false;
final bool isPlugin =
environment.getBool('FlutterProject.isPlugin') ?? false;
if (isModule || isPlugin) {
logger.printError(
'Migrate tool only supports app projects. This project is a ${isModule ? 'module' : 'plugin'}');
return const CommandResult(ExitStatus.fail);
}
final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone;
if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) {
return const CommandResult(ExitStatus.fail);
}
Directory stagingDirectory =
project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
final String? customStagingDirectoryPath = stringArg('staging-directory');
if (customStagingDirectoryPath != null) {
if (fileSystem.path.isAbsolute(customStagingDirectoryPath)) {
stagingDirectory = fileSystem.directory(customStagingDirectoryPath);
} else {
stagingDirectory =
project.directory.childDirectory(customStagingDirectoryPath);
}
}
if (stagingDirectory.existsSync()) {
logger.printStatus('Old migration already in progress.', emphasis: true);
logger.printStatus(
'Pending migration files exist in `${stagingDirectory.path}/$kDefaultMigrateStagingDirectoryName`');
logger.printStatus(
'Resolve merge conflicts and accept changes with by running:');
printCommandText('apply', logger, standalone: !isSubcommand);
logger.printStatus(
'You may also abandon the existing migration and start a new one with:');
printCommandText('abandon', logger, standalone: !isSubcommand);
return const CommandResult(ExitStatus.fail);
}
if (await hasUncommittedChanges(
project.directory.path, logger, migrateUtils)) {
return const CommandResult(ExitStatus.fail);
}
List<SupportedPlatform>? platforms;
if (stringArg('platforms') != null) {
platforms = <SupportedPlatform>[];
for (String platformString in stringArg('platforms')!.split(',')) {
platformString = platformString.trim();
platforms.add(SupportedPlatform.values.firstWhere(
(SupportedPlatform val) =>
val.toString() == 'SupportedPlatform.$platformString'));
}
}
final MigrateCommandParameters commandParameters = MigrateCommandParameters(
verbose: _verbose,
baseAppPath: stringArg('base-app-directory'),
targetAppPath: stringArg('target-app-directory'),
baseRevision: stringArg('base-revision'),
targetRevision: stringArg('target-revision'),
deleteTempDirectories: boolArg('delete-temp-directories') ?? true,
platforms: platforms,
preferTwoWayMerge: boolArg('prefer-two-way-merge') ?? false,
allowFallbackBaseRevision:
boolArg('allow-fallback-base-revision') ?? false,
);
final MigrateResult? migrateResult = await computeMigration(
flutterProject: project,
commandParameters: commandParameters,
fileSystem: fileSystem,
logger: logger,
migrateUtils: migrateUtils,
environment: environment,
);
if (migrateResult == null) {
return const CommandResult(ExitStatus.fail);
}
await writeStagingDir(migrateResult, logger,
verbose: _verbose, projectRootDir: projectRootDir);
_deleteTempDirectories(
paths: <String>[],
directories: migrateResult.tempDirectories,
);
logger.printStatus(
'The migrate tool has staged proposed changes in the migrate staging directory.\n');
logger.printStatus('Guided conflict resolution wizard:');
printCommandText('resolve-conflicts', logger, standalone: !isSubcommand);
logger.printStatus('Check the status and diffs of the migration with:');
printCommandText('status', logger, standalone: !isSubcommand);
logger.printStatus('Abandon the proposed migration with:');
printCommandText('abandon', logger, standalone: !isSubcommand);
logger.printStatus(
'Accept staged changes after resolving any merge conflicts with:');
printCommandText('apply', logger, standalone: !isSubcommand);
return const CommandResult(ExitStatus.success);
}
/// Deletes the files or directories at the provided paths.
void _deleteTempDirectories(
{List<String> paths = const <String>[],
List<Directory> directories = const <Directory>[]}) {
for (final Directory d in directories) {
try {
d.deleteSync(recursive: true);
} on FileSystemException catch (e) {
logger.printError(
'Unabled to delete ${d.path} due to ${e.message}, please clean up manually.');
}
}
for (final String p in paths) {
try {
fileSystem.directory(p).deleteSync(recursive: true);
} on FileSystemException catch (e) {
logger.printError(
'Unabled to delete $p due to ${e.message}, please clean up manually.');
}
}
}
bool _validateEnvironment(FlutterToolsEnvironment environment) {
if (environment.getString('FlutterProject.directory') == null) {
logger.printError(
'No valid flutter project found. This command must be run from a flutter project directory');
return false;
}
if (environment.getString('FlutterProject.manifest.appname') == null) {
logger.printError('No app name found in project pubspec.yaml');
return false;
}
if (!(environment.getBool('FlutterProject.android.exists') ?? false) &&
environment['FlutterProject.android.isKotlin'] == null) {
logger.printError(
'Could not detect if android project uses kotlin or java');
return false;
}
if (!(environment.getBool('FlutterProject.ios.exists') ?? false) &&
environment['FlutterProject.ios.isSwift'] == null) {
logger.printError(
'Could not detect if iosProject uses swift or objective-c');
return false;
}
return true;
}
/// Writes the files into the working directory for the developer to review and resolve any conflicts.
Future<void> writeStagingDir(MigrateResult migrateResult, Logger logger,
{bool verbose = false, required Directory projectRootDir}) async {
final Directory stagingDir =
projectRootDir.childDirectory(kDefaultMigrateStagingDirectoryName);
if (verbose) {
logger.printStatus(
'Writing migrate staging directory at `${stagingDir.path}`');
}
// Write files in working dir
for (final MergeResult result in migrateResult.mergeResults) {
final File file = stagingDir.childFile(result.localPath);
file.createSync(recursive: true);
if (result is StringMergeResult) {
file.writeAsStringSync(result.mergedString, flush: true);
} else {
file.writeAsBytesSync((result as BinaryMergeResult).mergedBytes,
flush: true);
}
}
// Write all files that are newly added in target
for (final FilePendingMigration addedFile in migrateResult.addedFiles) {
final File file = stagingDir.childFile(addedFile.localPath);
file.createSync(recursive: true);
try {
file.writeAsStringSync(addedFile.file.readAsStringSync(), flush: true);
} on FileSystemException {
file.writeAsBytesSync(addedFile.file.readAsBytesSync(), flush: true);
}
}
// Write the MigrateManifest.
final MigrateManifest manifest = MigrateManifest(
migrateRootDir: stagingDir,
migrateResult: migrateResult,
);
manifest.writeFile();
// output the manifest contents.
checkAndPrintMigrateStatus(manifest, stagingDir, logger: logger);
logger.printBox('Staging directory created at `${stagingDir.path}`');
}
}

View File

@ -20,6 +20,7 @@ class MigrateStatusCommand extends MigrateCommand {
required this.logger,
required this.fileSystem,
required ProcessManager processManager,
this.standalone = false,
}) : _verbose = verbose,
migrateUtils = MigrateUtils(
logger: logger,
@ -65,6 +66,8 @@ class MigrateStatusCommand extends MigrateCommand {
final MigrateUtils migrateUtils;
final bool standalone;
@override
final String name = 'status';
@ -86,7 +89,11 @@ class MigrateStatusCommand extends MigrateCommand {
? FlutterProject.current(fileSystem)
: flutterProjectFactory
.fromDirectory(fileSystem.directory(projectDirectory));
final bool isSubcommand = boolArg('flutter-subcommand') ?? false;
final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone;
if (!validateWorkingDirectory(project, logger)) {
return CommandResult.fail();
}
Directory stagingDirectory =
project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
@ -166,7 +173,7 @@ class MigrateStatusCommand extends MigrateCommand {
}
}
logger.printBox('Working directory at `${stagingDirectory.path}`');
logger.printBox('Staging directory at `${stagingDirectory.path}`');
checkAndPrintMigrateStatus(manifest, stagingDirectory, logger: logger);

View File

@ -55,11 +55,8 @@ bool _skipped(String localPath, FileSystem fileSystem,
}
}
if (skippedPrefixes != null) {
final Iterable<String> canonicalizedSkippedPrefixes =
_skippedFiles.map<String>((String path) => canonicalize(path));
return canonicalizedSkippedPrefixes.any((String prefix) =>
canonicalizedLocalPath
.startsWith('${canonicalize(prefix)}${fileSystem.path.separator}'));
return skippedPrefixes.any((String prefix) => localPath.startsWith(
'${normalize(prefix.replaceAll(r'\', fileSystem.path.separator))}${fileSystem.path.separator}'));
}
return false;
}
@ -114,7 +111,6 @@ Set<String> _getSkippedPrefixes(List<SupportedPlatform> platforms) {
for (final SupportedPlatform platform in platforms) {
skippedPrefixes.remove(platformToSubdirectoryPrefix(platform));
}
skippedPrefixes.remove(null);
return skippedPrefixes;
}
@ -275,10 +271,6 @@ Future<MigrateResult?> computeMigration({
platforms: platforms,
commandParameters: commandParameters,
);
result.generatedBaseTemplateDirectory =
referenceProjects.baseProject.directory;
result.generatedTargetTemplateDirectory =
referenceProjects.targetProject.directory;
// Generate diffs. These diffs are used to determine if a file is newly added, needs merging,
// or deleted (rare). Only files with diffs between the base and target revisions need to be
@ -377,8 +369,10 @@ Future<ReferenceProjects> _generateBaseAndTargetReferenceProjects({
// Use user-provided projects if provided, if not, generate them internally.
final bool customBaseProjectDir = commandParameters.baseAppPath != null;
final bool customTargetProjectDir = commandParameters.targetAppPath != null;
Directory? baseProjectDir;
Directory? targetProjectDir;
Directory baseProjectDir =
context.fileSystem.systemTempDirectory.createTempSync('baseProject');
Directory targetProjectDir =
context.fileSystem.systemTempDirectory.createTempSync('targetProject');
if (customBaseProjectDir) {
baseProjectDir =
context.fileSystem.directory(commandParameters.baseAppPath);
@ -402,6 +396,9 @@ Future<ReferenceProjects> _generateBaseAndTargetReferenceProjects({
await context.migrateUtils.gitInit(baseProjectDir.absolute.path);
await context.migrateUtils.gitInit(targetProjectDir.absolute.path);
result.generatedBaseTemplateDirectory = baseProjectDir;
result.generatedTargetTemplateDirectory = targetProjectDir;
final String name =
context.environment['FlutterProject.manifest.appname']! as String;
final String androidLanguage =
@ -821,7 +818,7 @@ class MigrateBaseFlutterProject extends MigrateFlutterProject {
final List<String> platforms = <String>[];
for (final MigratePlatformConfig config
in revisionToConfigs[revision]!) {
if (config.component == null) {
if (config.component == FlutterProjectComponent.root) {
continue;
}
platforms.add(config.component.toString().split('.').last);

View File

@ -220,6 +220,7 @@ class MigrateUtils {
'--deleted',
'--modified',
'--others',
'--exclude-standard',
'--exclude=${migrateStagingDir ?? kDefaultMigrateStagingDirectoryName}'
];
final ProcessResult result =
@ -332,7 +333,7 @@ Future<bool> gitRepoExists(
}
logger.printStatus(
'Project is not a git repo. Please initialize a git repo and try again.');
printCommandText('git init', logger);
printCommand('git init', logger);
return false;
}
@ -341,27 +342,34 @@ Future<bool> hasUncommittedChanges(
if (await migrateUtils.hasUncommittedChanges(projectDirectory)) {
logger.printStatus(
'There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.');
logger.printStatus(
'You may commit your changes using \'git add .\' and \'git commit -m "<message"\'');
logger.printStatus('You may commit your changes using');
printCommand('git add .', logger, newlineAfter: false);
printCommand('git commit -m "<message>"', logger);
return true;
}
return false;
}
/// Prints a command to logger with appropriate formatting.
void printCommandText(String command, Logger logger,
{bool? standalone = true}) {
final String prefix = standalone == null
? ''
: (standalone ? './bin/flutter_migrate ' : 'flutter migrate ');
void printCommand(String command, Logger logger, {bool newlineAfter = true}) {
logger.printStatus(
'\n\$ $prefix$command\n',
'\n\$ $command${newlineAfter ? '\n' : ''}',
color: TerminalColor.grey,
indent: 4,
newline: false,
);
}
/// Prints a command to logger with appropriate formatting.
void printCommandText(String command, Logger logger,
{bool? standalone = true, bool newlineAfter = true}) {
final String prefix = standalone == null
? ''
: (standalone
? 'dart run <flutter_migrate_dir>${Platform.pathSeparator}bin${Platform.pathSeparator}flutter_migrate.dart '
: 'flutter migrate ');
printCommand('$prefix$command', logger, newlineAfter: newlineAfter);
}
/// Defines the classification of difference between files.
enum DiffType {
command,

View File

@ -0,0 +1,440 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_migrate/src/base/common.dart';
import 'package:flutter_migrate/src/base/file_system.dart';
import 'package:flutter_migrate/src/base/io.dart';
import 'package:flutter_migrate/src/base/logger.dart';
import 'package:flutter_migrate/src/base/signals.dart';
import 'package:flutter_migrate/src/base/terminal.dart';
import 'package:process/process.dart';
import 'src/common.dart';
import 'src/context.dart';
import 'test_data/migrate_project.dart';
// This file contains E2E test that execute the core migrate commands
// and simulates manual conflict resolution and other manipulations of
// the project files.
void main() {
late Directory tempDir;
late BufferLogger logger;
late ProcessManager processManager;
late FileSystem fileSystem;
setUp(() async {
logger = BufferLogger.test();
processManager = const LocalProcessManager();
fileSystem = LocalFileSystem.test(signals: LocalSignals.instance);
tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_run_test');
});
tearDown(() async {
tryToDelete(tempDir);
});
Future<bool> hasFlutterEnvironment() async {
final String flutterRoot = getFlutterRoot();
final String flutterExecutable = fileSystem.path
.join(flutterRoot, 'bin', 'flutter${isWindows ? '.bat' : ''}');
final ProcessResult result = await Process.run(
flutterExecutable, <String>['analyze', '--suggestions', '--machine']);
if (result.exitCode != 0) {
return false;
}
return true;
}
// Migrates a clean untouched app generated with flutter create
testUsingContext('vanilla migrate process succeeds', () async {
// This tool does not support old versions of flutter that dont include
// `flutter analyze --suggestions --machine` command
if (!await hasFlutterEnvironment()) {
return;
}
// Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
await MigrateProject.installProject('version:1.22.6_stable', tempDir);
ProcessResult result = await runMigrateCommand(<String>[
'start',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.stdout.toString(), contains('Staging directory created at'));
const String linesToMatch = '''
Added files:
- android/app/src/main/res/values-night/styles.xml
- android/app/src/main/res/drawable-v21/launch_background.xml
- analysis_options.yaml
Modified files:
- .metadata
- ios/Runner/Info.plist
- ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
- ios/Flutter/AppFrameworkInfo.plist
- ios/.gitignore
- pubspec.yaml
- .gitignore
- android/app/build.gradle
- android/app/src/profile/AndroidManifest.xml
- android/app/src/main/res/values/styles.xml
- android/app/src/main/AndroidManifest.xml
- android/app/src/debug/AndroidManifest.xml
- android/gradle/wrapper/gradle-wrapper.properties
- android/.gitignore
- android/build.gradle''';
for (final String line in linesToMatch.split('\n')) {
expect(result.stdout.toString(), contains(line));
}
result = await runMigrateCommand(<String>[
'apply',
'--verbose',
], workingDirectory: tempDir.path);
logger.printStatus('${result.exitCode}', color: TerminalColor.blue);
logger.printStatus(result.stdout, color: TerminalColor.green);
logger.printStatus(result.stderr, color: TerminalColor.red);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('Migration complete'));
expect(tempDir.childFile('.metadata').readAsStringSync(),
contains('migration:\n platforms:\n - platform: root\n'));
expect(
tempDir
.childFile('android/app/src/main/res/values-night/styles.xml')
.existsSync(),
true);
expect(tempDir.childFile('analysis_options.yaml').existsSync(), true);
}, timeout: const Timeout(Duration(seconds: 500)), skip: isWindows);
// Migrates a clean untouched app generated with flutter create
testUsingContext('vanilla migrate builds', () async {
// This tool does not support old versions of flutter that dont include
// `flutter analyze --suggestions --machine` command
if (!await hasFlutterEnvironment()) {
return;
}
// Flutter Stable 2.0.0 hash: 60bd88df915880d23877bfc1602e8ddcf4c4dd2a
await MigrateProject.installProject('version:2.0.0_stable', tempDir,
main: '''
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Container(),
);
}
}
''');
ProcessResult result = await runMigrateCommand(<String>[
'start',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.stdout.toString(), contains('Staging directory created at'));
result = await runMigrateCommand(<String>[
'apply',
'--verbose',
], workingDirectory: tempDir.path);
logger.printStatus('${result.exitCode}', color: TerminalColor.blue);
logger.printStatus(result.stdout, color: TerminalColor.green);
logger.printStatus(result.stderr, color: TerminalColor.red);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('Migration complete'));
result = await processManager.run(<String>[
'flutter',
'build',
'apk',
'--debug',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('app-debug.apk'));
// Skipped due to being flaky, the build completes successfully, but sometimes
// Gradle crashes due to resources on the bot. We should fine tune this to
// make it stable.
}, timeout: const Timeout(Duration(seconds: 900)), skip: true);
testUsingContext('migrate abandon', () async {
// Abandon in an empty dir fails.
ProcessResult result = await runMigrateCommand(<String>[
'abandon',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stderr.toString(),
contains('Error: No pubspec.yaml file found'));
expect(
result.stderr.toString(),
contains(
'This command should be run from the root of your Flutter project'));
final File manifestFile =
tempDir.childFile('migrate_staging_dir/.migrate_manifest');
expect(manifestFile.existsSync(), false);
// Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
await MigrateProject.installProject('version:1.22.6_stable', tempDir);
// Initialized repo fails.
result = await runMigrateCommand(<String>[
'abandon',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('No migration in progress'));
// Create migration.
manifestFile.createSync(recursive: true);
// Directory with manifest_staging_dir succeeds.
result = await runMigrateCommand(<String>[
'abandon',
'--verbose',
'--force',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('Abandon complete'));
}, timeout: const Timeout(Duration(seconds: 300)));
// Migrates a user-modified app
testUsingContext('modified migrate process succeeds', () async {
// This tool does not support old versions of flutter that dont include
// `flutter analyze --suggestions --machine` command
if (!await hasFlutterEnvironment()) {
return;
}
// Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
await MigrateProject.installProject('version:1.22.6_stable', tempDir,
vanilla: false);
ProcessResult result = await runMigrateCommand(<String>[
'apply',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('No migration'));
result = await runMigrateCommand(<String>[
'status',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('No migration'));
result = await runMigrateCommand(<String>[
'start',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('Staging directory created at'));
const String linesToMatch = '''
Modified files:
- .metadata
- ios/Runner/Info.plist
- ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
- ios/Flutter/AppFrameworkInfo.plist
- ios/.gitignore
- .gitignore
- android/app/build.gradle
- android/app/src/profile/AndroidManifest.xml
- android/app/src/main/res/values/styles.xml
- android/app/src/main/AndroidManifest.xml
- android/app/src/debug/AndroidManifest.xml
- android/gradle/wrapper/gradle-wrapper.properties
- android/.gitignore
- android/build.gradle
Merge conflicted files:
- pubspec.yaml''';
for (final String line in linesToMatch.split('\n')) {
expect(result.stdout.toString(), contains(line));
}
// Call apply with conflicts remaining. Should fail.
result = await runMigrateCommand(<String>[
'apply',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(
result.stdout.toString(),
contains(
'Conflicting files found. Resolve these conflicts and try again.'));
expect(result.stdout.toString(), contains('- pubspec.yaml'));
result = await runMigrateCommand(<String>[
'status',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('Modified files'));
expect(result.stdout.toString(), contains('Merge conflicted files'));
// Manually resolve conflics. The correct contents for resolution may change over time,
// but it shouldnt matter for this test.
final File metadataFile =
tempDir.childFile('migrate_staging_dir/.metadata');
metadataFile.writeAsStringSync('''
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: e96a72392696df66755ca246ff291dfc6ca6c4ad
channel: unknown
project_type: app
''', flush: true);
final File pubspecYamlFile =
tempDir.childFile('migrate_staging_dir/pubspec.yaml');
pubspecYamlFile.writeAsStringSync('''
name: vanilla_app_1_22_6_stable
description: This is a modified description from the default.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.17.0-79.0.dev <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- images/a_dot_burr.jpeg
- images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
''', flush: true);
result = await runMigrateCommand(<String>[
'status',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('Modified files'));
expect(result.stdout.toString(), contains('diff --git'));
expect(result.stdout.toString(), contains('@@'));
expect(result.stdout.toString(), isNot(contains('Merge conflicted files')));
result = await runMigrateCommand(<String>[
'apply',
'--verbose',
], workingDirectory: tempDir.path);
expect(result.exitCode, 0);
expect(result.stdout.toString(), contains('Migration complete'));
expect(tempDir.childFile('.metadata').readAsStringSync(),
contains('e96a72392696df66755ca246ff291dfc6ca6c4ad'));
expect(tempDir.childFile('pubspec.yaml').readAsStringSync(),
isNot(contains('">=2.6.0 <3.0.0"')));
expect(tempDir.childFile('pubspec.yaml').readAsStringSync(),
contains('">=2.17.0-79.0.dev <3.0.0"'));
expect(
tempDir.childFile('pubspec.yaml').readAsStringSync(),
contains(
'description: This is a modified description from the default.'));
expect(tempDir.childFile('lib/main.dart').readAsStringSync(),
contains('OtherWidget()'));
expect(tempDir.childFile('lib/other.dart').existsSync(), true);
expect(tempDir.childFile('lib/other.dart').readAsStringSync(),
contains('class OtherWidget'));
expect(
tempDir
.childFile('android/app/src/main/res/values-night/styles.xml')
.existsSync(),
true);
expect(tempDir.childFile('analysis_options.yaml').existsSync(), true);
}, timeout: const Timeout(Duration(seconds: 500)), skip: isWindows);
}

View File

@ -56,7 +56,7 @@ class MigrateProject extends Project {
'git',
'commit',
'-m',
'"Initial commit"',
'"All changes"',
], workingDirectory: dir.path);
}
@ -101,18 +101,37 @@ class MigrateProject extends Project {
], workingDirectory: dir.path);
if (Platform.isWindows) {
await processManager.run(<String>[
ProcessResult res = await processManager.run(<String>[
'robocopy',
tempDir.path,
dir.path,
'*',
'/E',
'/V',
'/mov',
]);
// Add full access permissions to Users
await processManager.run(<String>[
// Robocopy exit code 1 means some files were copied. 0 means no files were copied.
assert(res.exitCode == 1);
res = await processManager.run(<String>[
'takeown',
'/f',
dir.path,
'/r',
]);
res = await processManager.run(<String>[
'takeown',
'/f',
'${dir.path}\\lib\\main.dart',
'/r',
]);
res = await processManager.run(<String>[
'icacls',
tempDir.path,
dir.path,
], workingDirectory: dir.path);
// Add full access permissions to Users
res = await processManager.run(<String>[
'icacls',
dir.path,
'/q',
'/c',
'/t',
@ -138,8 +157,16 @@ class MigrateProject extends Project {
await processManager.run(<String>[
'chmod',
'-R',
'+w',
'${dir.path}${fileSystem.path.separator}*',
dir.path,
], workingDirectory: dir.path);
await processManager.run(<String>[
'chmod',
'-R',
'+r',
dir.path,
], workingDirectory: dir.path);
}

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_migrate/src/base/common.dart';
import 'package:flutter_migrate/src/base/file_system.dart';
import 'package:flutter_migrate/src/base/io.dart';
import 'package:flutter_migrate/src/base/logger.dart';
@ -238,7 +239,17 @@ void main() {
logger.clear();
printCommandText('fullstandalone', logger);
expect(logger.statusText, contains('./bin/flutter_migrate fullstandalone'));
if (isWindows) {
expect(
logger.statusText,
contains(
r'dart run <flutter_migrate_dir>\bin\flutter_migrate.dart fullstandalone'));
} else {
expect(
logger.statusText,
contains(
'dart run <flutter_migrate_dir>/bin/flutter_migrate.dart fullstandalone'));
}
logger.clear();
});