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 {
final FlutterProject project;
bool existsSync() => project.directory.childDirectory('windows').existsSync();
bool existsSync() => _editableDirectory.existsSync();
// Note: The build script file exists as a temporary shim.
File get buildScript => project.directory.childDirectory('windows').childFile('build.bat');
Directory get _editableDirectory => project.directory.childDirectory('windows');
/// 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.
File get nameScript => project.directory.childDirectory('windows').childFile('name_output.bat');
File get nameScript => _editableDirectory.childFile('name_output.bat');
}
/// The Linux sub project.
......
......@@ -11,14 +11,26 @@ import '../cache.dart';
import '../convert.dart';
import '../globals.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 {
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>[
windowsProject.buildScript.path,
Cache.flutterRoot,
buildInfo.isDebug ? 'debug' : 'release',
buildInfo?.trackWidgetCreation == true ? 'track-widget-creation' : 'no-track-widget-creation',
vcvarsScript, '&&', 'msbuild',
windowsProject.vcprojFile.path,
'/p:Configuration=$configuration',
], runInShell: true);
final Status status = logger.startProgress(
'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';
import 'package:flutter_tools/src/commands/build.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:xml/xml.dart' as xml;
import '../src/common.dart';
import '../src/context.dart';
......@@ -21,8 +22,12 @@ void main() {
final MockProcessManager mockProcessManager = MockProcessManager();
final MemoryFileSystem memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows);
final MockProcess mockProcess = MockProcess();
final MockPlatform windowsPlatform = MockPlatform();
final MockPlatform windowsPlatform = MockPlatform()
..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\';
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 {
return 0;
......@@ -36,9 +41,22 @@ void main() {
when(windowsPlatform.isWindows).thenReturn(true);
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 {
final BuildCommand command = BuildCommand();
applyMocksToCommand(command);
fs.file(vcvarsPath).createSync(recursive: true);
expect(createTestCommandRunner(command).run(
const <String>['build', 'windows']
), throwsA(isInstanceOf<ToolExit>()));
......@@ -50,7 +68,8 @@ void main() {
testUsingContext('Windows build fails on non windows platform', () async {
final BuildCommand command = BuildCommand();
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('.packages').createSync();
......@@ -62,17 +81,20 @@ void main() {
FileSystem: () => memoryFilesystem,
});
testUsingContext('Windows build invokes build script', () async {
testUsingContext('Windows build invokes msbuild and writes generated files', () async {
final BuildCommand command = BuildCommand();
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('.packages').createSync();
when(mockProcessManager.start(<String>[
r'C:\windows\build.bat',
r'C:\',
'release',
'no-track-widget-creation',
vcvarsPath,
'&&',
'msbuild',
'C:\\$projectPath',
'/p:Configuration=Release',
], runInShell: true)).thenAnswer((Invocation invocation) async {
return mockProcess;
});
......@@ -80,6 +102,14 @@ void main() {
await createTestCommandRunner(command).run(
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>{
FileSystem: () => memoryFilesystem,
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