[Windows] Add version info migration (#123414)

[Windows] Add version info migration
This commit is contained in:
Loïc Sharma 2023-03-29 13:11:18 -07:00 committed by GitHub
parent f44064991d
commit 9f2ac97174
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 485 additions and 0 deletions

View file

@ -61,6 +61,13 @@ class WindowsProject extends FlutterProjectPlatform implements CmakeBasedProject
@override
File get generatedPluginCmakeFile => managedDirectory.childFile('generated_plugins.cmake');
/// The native entrypoint's CMake specification.
File get runnerCmakeFile => runnerDirectory.childFile('CMakeLists.txt');
/// The native entrypoint's resource file. Used to configure things
/// like the application icon, name, and version.
File get runnerResourceFile => runnerDirectory.childFile('Runner.rc');
@override
Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks');
@ -76,6 +83,11 @@ class WindowsProject extends FlutterProjectPlatform implements CmakeBasedProject
/// checked in should live here.
Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
/// The directory in the project that is owned by the app. As much as
/// possible, Flutter tooling should not edit files in this directory after
/// initial project creation.
Directory get runnerDirectory => _editableDirectory.childDirectory('runner');
Future<void> ensureReadyForPlatformSpecificTooling() async {}
}

View file

@ -18,6 +18,7 @@ import '../convert.dart';
import '../flutter_plugins.dart';
import '../globals.dart' as globals;
import '../migrations/cmake_custom_command_migration.dart';
import 'migrations/version_migration.dart';
import 'visual_studio.dart';
// These characters appear to be fine: @%()-+_{}[]`~
@ -52,6 +53,7 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {
final List<ProjectMigrator> migrators = <ProjectMigrator>[
CmakeCustomCommandMigration(windowsProject, globals.logger),
VersionMigration(windowsProject, globals.logger),
];
final ProjectMigration migration = ProjectMigration(migrators);

View file

@ -0,0 +1,147 @@
// Copyright 2014 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 '../../base/file_system.dart';
import '../../base/project_migrator.dart';
import '../../cmake_project.dart';
const String _cmakeFileBefore = r'''
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
''';
const String _cmakeFileAfter = r'''
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
''';
const String _resourceFileBefore = '''
#ifdef FLUTTER_BUILD_NUMBER
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
#else
#define VERSION_AS_NUMBER 1,0,0
#endif
#ifdef FLUTTER_BUILD_NAME
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
#else
#define VERSION_AS_STRING "1.0.0"
#endif
''';
const String _resourceFileAfter = '''
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
''';
/// Migrates Windows apps to set Flutter's version information.
///
/// See https://github.com/flutter/flutter/issues/73652.
class VersionMigration extends ProjectMigrator {
VersionMigration(WindowsProject project, super.logger)
: _cmakeFile = project.runnerCmakeFile, _resourceFile = project.runnerResourceFile;
final File _cmakeFile;
final File _resourceFile;
@override
void migrate() {
// Skip this migration if the affected files do not exist. This indicates
// the app has done non-trivial changes to its runner and this migration
// might not work as expected if applied.
if (!_cmakeFile.existsSync()) {
logger.printTrace('''
windows/runner/CMakeLists.txt file not found, skipping version migration.
This indicates non-trivial changes have been made to the Windows runner in the
"windows" folder. If needed, you can reset the Windows runner by deleting the
"windows" folder and then using the "flutter create --platforms=windows ." command.
''');
return;
}
if (!_resourceFile.existsSync()) {
logger.printTrace('''
windows/runner/Runner.rc file not found, skipping version migration.
This indicates non-trivial changes have been made to the Windows runner in the
"windows" folder. If needed, you can reset the Windows runner by deleting the
"windows" folder and then using the "flutter create --platforms=windows ." command.
''');
return;
}
// Migrate the windows/runner/CMakeLists.txt file.
final String originalCmakeContents = _cmakeFile.readAsStringSync();
final String newCmakeContents = _replaceFirst(
originalCmakeContents,
_cmakeFileBefore,
_cmakeFileAfter,
);
if (originalCmakeContents != newCmakeContents) {
logger.printStatus('windows/runner/CMakeLists.txt does not define version information, updating.');
_cmakeFile.writeAsStringSync(newCmakeContents);
}
// Migrate the windows/runner/Runner.rc file.
final String originalResourceFileContents = _resourceFile.readAsStringSync();
final String newResourceFileContents = _replaceFirst(
originalResourceFileContents,
_resourceFileBefore,
_resourceFileAfter,
);
if (originalResourceFileContents != newResourceFileContents) {
logger.printStatus(
'windows/runner/Runner.rc does not define use Flutter version information, updating.',
);
_resourceFile.writeAsStringSync(newResourceFileContents);
}
}
}
/// Creates a new string with the first occurrence of [before] replaced by
/// [after].
///
/// If the [originalContents] uses CRLF line endings, the [before] and [after]
/// will be converted to CRLF line endings before the replacement is made.
/// This is necessary for users that have git autocrlf enabled.
///
/// Example:
/// ```dart
/// 'a\n'.replaceFirst('a\n', 'b\n'); // 'b\n'
/// 'a\r\n'.replaceFirst('a\n', 'b\n'); // 'b\r\n'
/// ```
String _replaceFirst(String originalContents, String before, String after) {
final String result = originalContents.replaceFirst(before, after);
if (result != originalContents) {
return result;
}
final String beforeCrlf = before.replaceAll('\n', '\r\n');
final String afterCrlf = after.replaceAll('\n', '\r\n');
return originalContents.replaceFirst(beforeCrlf, afterCrlf);
}

View file

@ -0,0 +1,324 @@
// Copyright 2014 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:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cmake_project.dart';
import 'package:flutter_tools/src/windows/migrations/version_migration.dart';
import 'package:test/fake.dart';
import '../../../src/common.dart';
void main () {
group('Windows Flutter version migration', () {
late MemoryFileSystem memoryFileSystem;
late BufferLogger testLogger;
late FakeWindowsProject mockProject;
late File cmakeFile;
late File resourceFile;
setUp(() {
memoryFileSystem = MemoryFileSystem.test();
cmakeFile = memoryFileSystem.file('CMakeLists.txt');
resourceFile = memoryFileSystem.file('Runner.rc');
testLogger = BufferLogger(
terminal: Terminal.test(),
outputPreferences: OutputPreferences.test(),
);
mockProject = FakeWindowsProject(cmakeFile, resourceFile);
});
testWithoutContext('skipped if CMake file is missing', () {
const String resourceFileContents = 'Hello world';
resourceFile.writeAsStringSync(resourceFileContents);
final VersionMigration migration = VersionMigration(
mockProject,
testLogger,
);
migration.migrate();
expect(cmakeFile.existsSync(), isFalse);
expect(resourceFile.existsSync(), isTrue);
expect(testLogger.traceText,
contains('windows/runner/CMakeLists.txt file not found, skipping version migration'));
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if resource file is missing', () {
const String cmakeFileContents = 'Hello world';
cmakeFile.writeAsStringSync(cmakeFileContents);
final VersionMigration migration = VersionMigration(
mockProject,
testLogger,
);
migration.migrate();
expect(cmakeFile.existsSync(), isTrue);
expect(resourceFile.existsSync(), isFalse);
expect(testLogger.traceText,
contains('windows/runner/Runner.rc file not found, skipping version migration'));
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if nothing to migrate', () {
const String cmakeFileContents = 'Nothing to migrate';
const String resourceFileContents = 'Nothing to migrate';
cmakeFile.writeAsStringSync(cmakeFileContents);
resourceFile.writeAsStringSync(resourceFileContents);
final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync();
final DateTime resourceUpdatedAt = resourceFile.lastModifiedSync();
final VersionMigration versionMigration = VersionMigration(
mockProject,
testLogger,
);
versionMigration.migrate();
expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt);
expect(cmakeFile.readAsStringSync(), cmakeFileContents);
expect(resourceFile.lastModifiedSync(), resourceUpdatedAt);
expect(resourceFile.readAsStringSync(), resourceFileContents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if already migrated', () {
const String cmakeFileContents =
'# Apply the standard set of build settings. This can be removed for applications\n'
'# that need different build settings.\n'
'apply_standard_settings(\${BINARY_NAME})\n'
'\n'
'# Add preprocessor definitions for the build version.\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\n'
'\n'
'# Disable Windows macros that collide with C++ standard library functions.\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\n';
const String resourceFileContents =
'#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n'
'#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n'
'#else\n'
'#define VERSION_AS_NUMBER 1,0,0,0\n'
'#endif\n'
'\n'
'#if defined(FLUTTER_VERSION)\n'
'#define VERSION_AS_STRING FLUTTER_VERSION\n'
'#else\n'
'#define VERSION_AS_STRING "1.0.0"\n'
'#endif\n';
cmakeFile.writeAsStringSync(cmakeFileContents);
resourceFile.writeAsStringSync(resourceFileContents);
final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync();
final DateTime resourceUpdatedAt = resourceFile.lastModifiedSync();
final VersionMigration versionMigration = VersionMigration(
mockProject,
testLogger,
);
versionMigration.migrate();
expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt);
expect(cmakeFile.readAsStringSync(), cmakeFileContents);
expect(resourceFile.lastModifiedSync(), resourceUpdatedAt);
expect(resourceFile.readAsStringSync(), resourceFileContents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if already migrated (CRLF)', () {
const String cmakeFileContents =
'# Apply the standard set of build settings. This can be removed for applications\r\n'
'# that need different build settings.\r\n'
'apply_standard_settings(\${BINARY_NAME})\r\n'
'\r\n'
'# Add preprocessor definitions for the build version.\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\r\n'
'\r\n'
'# Disable Windows macros that collide with C++ standard library functions.\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\r\n';
const String resourceFileContents =
'#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\r\n'
'#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\r\n'
'#else\r\n'
'#define VERSION_AS_NUMBER 1,0,0,0\r\n'
'#endif\r\n'
'\r\n'
'#if defined(FLUTTER_VERSION)\r\n'
'#define VERSION_AS_STRING FLUTTER_VERSION\r\n'
'#else\r\n'
'#define VERSION_AS_STRING "1.0.0"\r\n'
'#endif\r\n';
cmakeFile.writeAsStringSync(cmakeFileContents);
resourceFile.writeAsStringSync(resourceFileContents);
final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync();
final DateTime resourceUpdatedAt = resourceFile.lastModifiedSync();
final VersionMigration versionMigration = VersionMigration(
mockProject,
testLogger,
);
versionMigration.migrate();
expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt);
expect(cmakeFile.readAsStringSync(), cmakeFileContents);
expect(resourceFile.lastModifiedSync(), resourceUpdatedAt);
expect(resourceFile.readAsStringSync(), resourceFileContents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('migrates project to set version information', () {
cmakeFile.writeAsStringSync(
'# Apply the standard set of build settings. This can be removed for applications\n'
'# that need different build settings.\n'
'apply_standard_settings(\${BINARY_NAME})\n'
'\n'
'# Disable Windows macros that collide with C++ standard library functions.\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\n'
);
resourceFile.writeAsStringSync(
'#ifdef FLUTTER_BUILD_NUMBER\n'
'#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\n'
'#else\n'
'#define VERSION_AS_NUMBER 1,0,0\n'
'#endif\n'
'\n'
'#ifdef FLUTTER_BUILD_NAME\n'
'#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\n'
'#else\n'
'#define VERSION_AS_STRING "1.0.0"\n'
'#endif\n'
);
final VersionMigration versionMigration = VersionMigration(
mockProject,
testLogger,
);
versionMigration.migrate();
expect(cmakeFile.readAsStringSync(),
'# Apply the standard set of build settings. This can be removed for applications\n'
'# that need different build settings.\n'
'apply_standard_settings(\${BINARY_NAME})\n'
'\n'
'# Add preprocessor definitions for the build version.\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\n'
'\n'
'# Disable Windows macros that collide with C++ standard library functions.\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\n'
);
expect(resourceFile.readAsStringSync(),
'#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n'
'#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n'
'#else\n'
'#define VERSION_AS_NUMBER 1,0,0,0\n'
'#endif\n'
'\n'
'#if defined(FLUTTER_VERSION)\n'
'#define VERSION_AS_STRING FLUTTER_VERSION\n'
'#else\n'
'#define VERSION_AS_STRING "1.0.0"\n'
'#endif\n'
);
expect(testLogger.statusText, contains('windows/runner/CMakeLists.txt does not define version information, updating.'));
expect(testLogger.statusText, contains('windows/runner/Runner.rc does not define use Flutter version information, updating.'));
});
testWithoutContext('migrates project to set version information (CRLF)', () {
cmakeFile.writeAsStringSync(
'# Apply the standard set of build settings. This can be removed for applications\r\n'
'# that need different build settings.\r\n'
'apply_standard_settings(\${BINARY_NAME})\r\n'
'\r\n'
'# Disable Windows macros that collide with C++ standard library functions.\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\r\n'
);
resourceFile.writeAsStringSync(
'#ifdef FLUTTER_BUILD_NUMBER\r\n'
'#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\r\n'
'#else\r\n'
'#define VERSION_AS_NUMBER 1,0,0\r\n'
'#endif\r\n'
'\r\n'
'#ifdef FLUTTER_BUILD_NAME\r\n'
'#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\r\n'
'#else\r\n'
'#define VERSION_AS_STRING "1.0.0"\r\n'
'#endif\r\n'
);
final VersionMigration versionMigration = VersionMigration(
mockProject,
testLogger,
);
versionMigration.migrate();
expect(cmakeFile.readAsStringSync(),
'# Apply the standard set of build settings. This can be removed for applications\r\n'
'# that need different build settings.\r\n'
'apply_standard_settings(\${BINARY_NAME})\r\n'
'\r\n'
'# Add preprocessor definitions for the build version.\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\r\n'
'\r\n'
'# Disable Windows macros that collide with C++ standard library functions.\r\n'
'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\r\n'
);
expect(resourceFile.readAsStringSync(),
'#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\r\n'
'#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\r\n'
'#else\r\n'
'#define VERSION_AS_NUMBER 1,0,0,0\r\n'
'#endif\r\n'
'\r\n'
'#if defined(FLUTTER_VERSION)\r\n'
'#define VERSION_AS_STRING FLUTTER_VERSION\r\n'
'#else\r\n'
'#define VERSION_AS_STRING "1.0.0"\r\n'
'#endif\r\n'
);
expect(testLogger.statusText, contains('windows/runner/CMakeLists.txt does not define version information, updating.'));
expect(testLogger.statusText, contains('windows/runner/Runner.rc does not define use Flutter version information, updating.'));
});
});
}
class FakeWindowsProject extends Fake implements WindowsProject {
FakeWindowsProject(this.runnerCmakeFile, this.runnerResourceFile);
@override
final File runnerCmakeFile;
@override
final File runnerResourceFile;
}