Unverified Commit 6722fb44 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Teach flutter msbuild for Windows (#32335)

Eliminates the need for a build.bat in the Windows build workflow, adding
preliminary support for building using msbuild. The handling of
vcvars64.bat may be refined in the future, but this serves as a starting point.
parent 6d4b0abf
...@@ -592,13 +592,19 @@ class WindowsProject { ...@@ -592,13 +592,19 @@ class WindowsProject {
final FlutterProject project; final FlutterProject project;
bool existsSync() => project.directory.childDirectory('windows').existsSync(); bool existsSync() => _editableDirectory.existsSync();
// Note: The build script file exists as a temporary shim. Directory get _editableDirectory => project.directory.childDirectory('windows');
File get buildScript => project.directory.childDirectory('windows').childFile('build.bat');
/// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
/// the build.
File get generatedPropertySheetFile => _editableDirectory.childDirectory('flutter').childFile('Generated.props');
// The MSBuild project file.
File get vcprojFile => _editableDirectory.childFile('Runner.vcxproj');
// Note: The name script file exists as a temporary shim. // Note: The name script file exists as a temporary shim.
File get nameScript => project.directory.childDirectory('windows').childFile('name_output.bat'); File get nameScript => _editableDirectory.childFile('name_output.bat');
} }
/// The Linux sub project. /// The Linux sub project.
......
...@@ -11,14 +11,26 @@ import '../cache.dart'; ...@@ -11,14 +11,26 @@ import '../cache.dart';
import '../convert.dart'; import '../convert.dart';
import '../globals.dart'; import '../globals.dart';
import '../project.dart'; import '../project.dart';
import 'msbuild_utils.dart';
/// Builds the Windows project through the project bat script. /// Builds the Windows project using msbuild.
Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo) async { Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo) async {
final Map<String, String> environment = <String, String>{
'FLUTTER_ROOT': Cache.flutterRoot,
'EXTRA_BUNDLE_FLAGS': buildInfo?.trackWidgetCreation == true ? '--track-widget-creation' : '',
};
writePropertySheet(windowsProject.generatedPropertySheetFile, environment);
final String vcvarsScript = await findVcvars();
if (vcvarsScript == null) {
throwToolExit('Unable to build: could not find vcvars64.bat');
}
final String configuration = buildInfo.isDebug ? 'Debug' : 'Release';
final Process process = await processManager.start(<String>[ final Process process = await processManager.start(<String>[
windowsProject.buildScript.path, vcvarsScript, '&&', 'msbuild',
Cache.flutterRoot, windowsProject.vcprojFile.path,
buildInfo.isDebug ? 'debug' : 'release', '/p:Configuration=$configuration',
buildInfo?.trackWidgetCreation == true ? 'track-widget-creation' : 'no-track-widget-creation',
], runInShell: true); ], runInShell: true);
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Building Windows application...', 'Building Windows application...',
......
// Copyright 2019 The Chromium 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:xml/xml.dart' as xml;
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/platform.dart';
import '../base/process_manager.dart';
/// The supported versions of Visual Studio.
const List<String> _visualStudioVersions = <String>['2017', '2019'];
/// The supported flavors of Visual Studio.
const List<String> _visualStudioFlavors = <String>[
'Community',
'Professional',
'Enterprise',
'Preview'
];
/// Returns the path to an installed vcvars64.bat script if found, or null.
Future<String> findVcvars() async {
final String programDir = platform.environment['PROGRAMFILES(X86)'];
final String pathPrefix = fs.path.join(programDir, 'Microsoft Visual Studio');
const String vcvarsScriptName = 'vcvars64.bat';
final String pathSuffix =
fs.path.join('VC', 'Auxiliary', 'Build', vcvarsScriptName);
for (final String version in _visualStudioVersions) {
for (final String flavor in _visualStudioFlavors) {
final String testPath =
fs.path.join(pathPrefix, version, flavor, pathSuffix);
if (fs.file(testPath).existsSync()) {
return testPath;
}
}
}
// If it can't be found manually, check the path.
final ProcessResult whereResult = await processManager.run(<String>[
'where.exe',
vcvarsScriptName,
]);
if (whereResult.exitCode == 0) {
return whereResult.stdout.trim();
}
return null;
}
/// Writes a property sheet (.props) file to expose all of the key/value
/// pairs in [variables] as enivornment variables.
void writePropertySheet(File propertySheetFile, Map<String, String> variables) {
final xml.XmlBuilder builder = xml.XmlBuilder();
builder.processing('xml', 'version="1.0" encoding="utf-8"');
builder.element('Project', nest: () {
builder.attribute('ToolsVersion', '4.0');
builder.attribute(
'xmlns', 'http://schemas.microsoft.com/developer/msbuild/2003');
builder.element('ImportGroup', nest: () {
builder.attribute('Label', 'PropertySheets');
});
_addUserMacros(builder, variables);
builder.element('PropertyGroup');
builder.element('ItemDefinitionGroup');
_addItemGroup(builder, variables);
});
propertySheetFile.createSync(recursive: true);
propertySheetFile.writeAsStringSync(
builder.build().toXmlString(pretty: true, indent: ' '));
}
/// Adds the UserMacros PropertyGroup that defines [variables] to [builder].
void _addUserMacros(xml.XmlBuilder builder, Map<String, String> variables) {
builder.element('PropertyGroup', nest: () {
builder.attribute('Label', 'UserMacros');
for (final MapEntry<String, String> variable in variables.entries) {
builder.element(variable.key, nest: () {
builder.text(variable.value);
});
}
});
}
/// Adds the ItemGroup to expose the given [variables] as environment variables
/// to [builder].
void _addItemGroup(xml.XmlBuilder builder, Map<String, String> variables) {
builder.element('ItemGroup', nest: () {
for (final String name in variables.keys) {
builder.element('BuildMacro', nest: () {
builder.attribute('Include', name);
builder.element('Value', nest: () {
builder.text('\$($name)');
});
builder.element('EnvironmentVariable', nest: () {
builder.text('true');
});
});
}
});
}
...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/cache.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/commands/build.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import 'package:xml/xml.dart' as xml;
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -21,8 +22,12 @@ void main() { ...@@ -21,8 +22,12 @@ void main() {
final MockProcessManager mockProcessManager = MockProcessManager(); final MockProcessManager mockProcessManager = MockProcessManager();
final MemoryFileSystem memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows); final MemoryFileSystem memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows);
final MockProcess mockProcess = MockProcess(); final MockProcess mockProcess = MockProcess();
final MockPlatform windowsPlatform = MockPlatform(); final MockPlatform windowsPlatform = MockPlatform()
..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\';
final MockPlatform notWindowsPlatform = MockPlatform(); final MockPlatform notWindowsPlatform = MockPlatform();
const String projectPath = r'windows\Runner.vcxproj';
// A vcvars64.bat location that will be found by the lookup method.
const String vcvarsPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat';
when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { when(mockProcess.exitCode).thenAnswer((Invocation invocation) async {
return 0; return 0;
...@@ -36,9 +41,22 @@ void main() { ...@@ -36,9 +41,22 @@ void main() {
when(windowsPlatform.isWindows).thenReturn(true); when(windowsPlatform.isWindows).thenReturn(true);
when(notWindowsPlatform.isWindows).thenReturn(false); when(notWindowsPlatform.isWindows).thenReturn(false);
testUsingContext('Windows build fails when there is no vcvars64.bat', () async {
final BuildCommand command = BuildCommand();
applyMocksToCommand(command);
fs.file(projectPath).createSync(recursive: true);
expect(createTestCommandRunner(command).run(
const <String>['build', 'windows']
), throwsA(isInstanceOf<ToolExit>()));
}, overrides: <Type, Generator>{
Platform: () => windowsPlatform,
FileSystem: () => memoryFilesystem,
});
testUsingContext('Windows build fails when there is no windows project', () async { testUsingContext('Windows build fails when there is no windows project', () async {
final BuildCommand command = BuildCommand(); final BuildCommand command = BuildCommand();
applyMocksToCommand(command); applyMocksToCommand(command);
fs.file(vcvarsPath).createSync(recursive: true);
expect(createTestCommandRunner(command).run( expect(createTestCommandRunner(command).run(
const <String>['build', 'windows'] const <String>['build', 'windows']
), throwsA(isInstanceOf<ToolExit>())); ), throwsA(isInstanceOf<ToolExit>()));
...@@ -50,7 +68,8 @@ void main() { ...@@ -50,7 +68,8 @@ void main() {
testUsingContext('Windows build fails on non windows platform', () async { testUsingContext('Windows build fails on non windows platform', () async {
final BuildCommand command = BuildCommand(); final BuildCommand command = BuildCommand();
applyMocksToCommand(command); applyMocksToCommand(command);
fs.file(r'windows\build.bat').createSync(recursive: true); fs.file(projectPath).createSync(recursive: true);
fs.file(vcvarsPath).createSync(recursive: true);
fs.file('pubspec.yaml').createSync(); fs.file('pubspec.yaml').createSync();
fs.file('.packages').createSync(); fs.file('.packages').createSync();
...@@ -62,17 +81,20 @@ void main() { ...@@ -62,17 +81,20 @@ void main() {
FileSystem: () => memoryFilesystem, FileSystem: () => memoryFilesystem,
}); });
testUsingContext('Windows build invokes build script', () async { testUsingContext('Windows build invokes msbuild and writes generated files', () async {
final BuildCommand command = BuildCommand(); final BuildCommand command = BuildCommand();
applyMocksToCommand(command); applyMocksToCommand(command);
fs.file(r'windows\build.bat').createSync(recursive: true); fs.file(projectPath).createSync(recursive: true);
fs.file(vcvarsPath).createSync(recursive: true);
fs.file('pubspec.yaml').createSync(); fs.file('pubspec.yaml').createSync();
fs.file('.packages').createSync(); fs.file('.packages').createSync();
when(mockProcessManager.start(<String>[ when(mockProcessManager.start(<String>[
r'C:\windows\build.bat', vcvarsPath,
r'C:\', '&&',
'release', 'msbuild',
'no-track-widget-creation', 'C:\\$projectPath',
'/p:Configuration=Release',
], runInShell: true)).thenAnswer((Invocation invocation) async { ], runInShell: true)).thenAnswer((Invocation invocation) async {
return mockProcess; return mockProcess;
}); });
...@@ -80,6 +102,14 @@ void main() { ...@@ -80,6 +102,14 @@ void main() {
await createTestCommandRunner(command).run( await createTestCommandRunner(command).run(
const <String>['build', 'windows'] const <String>['build', 'windows']
); );
// Spot-check important elemenst from the properties file.
final File propsFile = fs.file(r'C:\windows\flutter\Generated.props');
expect(propsFile.existsSync(), true);
final xml.XmlDocument props = xml.parse(propsFile.readAsStringSync());
expect(props.findAllElements('PropertyGroup').first.getAttribute('Label'), 'UserMacros');
expect(props.findAllElements('ItemGroup').length, 1);
expect(props.findAllElements('FLUTTER_ROOT').first.text, r'C:\');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => memoryFilesystem, FileSystem: () => memoryFilesystem,
ProcessManager: () => mockProcessManager, ProcessManager: () => mockProcessManager,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment