build_windows.dart 11.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2
// Use of this source code is governed by a BSD-style license that can be
Ian Hickson's avatar
Ian Hickson committed
3
// found in the LICENSE file.
4

5
import '../artifacts.dart';
6
import '../base/analyze_size.dart';
7
import '../base/common.dart';
8
import '../base/file_system.dart';
9
import '../base/logger.dart';
10
import '../base/project_migrator.dart';
11
import '../base/terminal.dart';
12
import '../base/utils.dart';
13 14
import '../build_info.dart';
import '../cache.dart';
15
import '../cmake.dart';
16
import '../cmake_project.dart';
17
import '../convert.dart';
18
import '../flutter_plugins.dart';
19
import '../globals.dart' as globals;
20
import '../migrations/cmake_custom_command_migration.dart';
21
import 'migrations/version_migration.dart';
22
import 'visual_studio.dart';
23

24 25 26
// These characters appear to be fine: @%()-+_{}[]`~
const String _kBadCharacters = r"'#!$^&*=|,;<>?";

27
/// Builds the Windows project using msbuild.
28
Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {
29 30 31
  String? target,
  VisualStudio? visualStudioOverride,
  SizeAnalyzer? sizeAnalyzer,
32
}) async {
33 34 35 36 37 38 39 40 41 42 43 44 45 46
  // MSBuild files generated by CMake do not properly escape some characters
  // In the directories. This check produces more meaningful error messages
  // on failure as pertains to https://github.com/flutter/flutter/issues/104802
  final String projectPath = windowsProject.parent.directory.absolute.path;
  final bool badPath = _kBadCharacters.runes
      .any((int i) => projectPath.contains(String.fromCharCode(i)));
  if (badPath) {
    throwToolExit(
      'Path $projectPath contains invalid characters in "$_kBadCharacters". '
      'Please rename your directory so as to not include any of these characters '
      'and retry.',
    );
  }

47
  if (!windowsProject.cmakeFile.existsSync()) {
48
    throwToolExit(
49
      'No Windows desktop project configured. See '
50
      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
51 52 53
      'to learn about adding Windows support to a project.');
  }

54 55
  final List<ProjectMigrator> migrators = <ProjectMigrator>[
    CmakeCustomCommandMigration(windowsProject, globals.logger),
56
    VersionMigration(windowsProject, globals.logger),
57 58 59
  ];

  final ProjectMigration migration = ProjectMigration(migrators);
60
  migration.run();
61

62
  // Ensure that necessary ephemeral files are generated and up to date.
63 64
  _writeGeneratedFlutterConfig(windowsProject, buildInfo, target);
  createPluginSymlinks(windowsProject.parent);
65

66 67 68 69 70 71
  final VisualStudio visualStudio = visualStudioOverride ?? VisualStudio(
    fileSystem: globals.fs,
    platform: globals.platform,
    logger: globals.logger,
    processManager: globals.processManager,
  );
72
  final String? cmakePath = visualStudio.cmakePath;
73 74
  final String? cmakeGenerator = visualStudio.cmakeGenerator;
  if (cmakePath == null || cmakeGenerator == null) {
75 76
    throwToolExit('Unable to find suitable Visual Studio toolchain. '
        'Please run `flutter doctor` for more details.');
77 78
  }

79
  final String buildModeName = getNameForBuildMode(buildInfo.mode);
80
  final Directory buildDirectory = globals.fs.directory(getWindowsBuildDirectory());
81
  final Status status = globals.logger.startProgress(
82 83
    'Building Windows application...',
  );
84
  try {
85 86 87 88 89 90
    await _runCmakeGeneration(
      cmakePath: cmakePath,
      generator: cmakeGenerator,
      buildDir: buildDirectory,
      sourceDir: windowsProject.cmakeFile.parent,
    );
91 92 93
    if (visualStudio.displayVersion == '17.1.0') {
      _fixBrokenCmakeGeneration(buildDirectory);
    }
94 95
    await _runBuild(cmakePath, buildDirectory, buildModeName);
  } finally {
96
    status.stop();
97
  }
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

  final String? binaryName = getCmakeExecutableName(windowsProject);
  final File appFile = buildDirectory
    .childDirectory('runner')
    .childDirectory(sentenceCase(buildModeName))
    .childFile('$binaryName.exe');
  if (appFile.existsSync()) {
    final String appSize = (buildInfo.mode == BuildMode.debug)
        ? '' // Don't display the size when building a debug variant.
        : ' (${getSizeAsMB(appFile.lengthSync())})';
    globals.logger.printStatus(
      '${globals.logger.terminal.successMark}  '
      'Built ${globals.fs.path.relative(appFile.path)}$appSize.',
      color: TerminalColor.green,
    );
  }

115 116 117 118 119 120
  if (buildInfo.codeSizeDirectory != null && sizeAnalyzer != null) {
    final String arch = getNameForTargetPlatform(TargetPlatform.windows_x64);
    final File codeSizeFile = globals.fs.directory(buildInfo.codeSizeDirectory)
      .childFile('snapshot.$arch.json');
    final File precompilerTrace = globals.fs.directory(buildInfo.codeSizeDirectory)
      .childFile('trace.$arch.json');
121
    final Map<String, Object?> output = await sizeAnalyzer.analyzeAotSnapshot(
122 123 124 125 126 127 128 129 130
      aotSnapshot: codeSizeFile,
      // This analysis is only supported for release builds.
      outputDirectory: globals.fs.directory(
        globals.fs.path.join(getWindowsBuildDirectory(), 'runner', 'Release'),
      ),
      precompilerTrace: precompilerTrace,
      type: 'windows',
    );
    final File outputFile = globals.fsUtils.getUniqueFile(
131 132 133
      globals.fs
        .directory(globals.fsUtils.homeDirPath)
        .childDirectory('.flutter-devtools'), 'windows-code-size-analysis', 'json',
134 135 136 137 138
    )..writeAsStringSync(jsonEncode(output));
    // This message is used as a sentinel in analyze_apk_size_test.dart
    globals.printStatus(
      'A summary of your Windows bundle analysis can be found at: ${outputFile.path}',
    );
139 140 141 142 143

    // DevTools expects a file path relative to the .flutter-devtools/ dir.
    final String relativeAppSizePath = outputFile.path.split('.flutter-devtools/').last.trim();
    globals.printStatus(
      '\nTo analyze your app size in Dart DevTools, run the following command:\n'
144
      'dart devtools --appSizeBase=$relativeAppSizePath'
145
    );
146
  }
147 148
}

149 150 151 152 153 154
Future<void> _runCmakeGeneration({
  required String cmakePath,
  required String generator,
  required Directory buildDir,
  required Directory sourceDir,
}) async {
155 156 157 158 159
  final Stopwatch sw = Stopwatch()..start();

  await buildDir.create(recursive: true);
  int result;
  try {
160
    result = await globals.processUtils.stream(
161 162 163 164 165 166 167
      <String>[
        cmakePath,
        '-S',
        sourceDir.path,
        '-B',
        buildDir.path,
        '-G',
168
        generator,
169 170 171
      ],
      trace: true,
    );
172
  } on ArgumentError {
173 174 175 176 177 178 179 180
    throwToolExit("cmake not found. Run 'flutter doctor' for more information.");
  }
  if (result != 0) {
    throwToolExit('Unable to generate build files');
  }
  globals.flutterUsage.sendTiming('build', 'windows-cmake-generation', Duration(milliseconds: sw.elapsedMilliseconds));
}

181 182 183 184 185 186
Future<void> _runBuild(
  String cmakePath,
  Directory buildDir,
  String buildModeName,
  { bool install = true }
) async {
187 188
  final Stopwatch sw = Stopwatch()..start();

189 190 191 192
  // MSBuild sends all output to stdout, including build errors. This surfaces
  // known error patterns.
  final RegExp errorMatcher = RegExp(r':\s*(?:warning|(?:fatal )?error).*?:');

193 194
  int result;
  try {
195
    result = await globals.processUtils.stream(
196
      <String>[
197 198 199 200
        cmakePath,
        '--build',
        buildDir.path,
        '--config',
201
        sentenceCase(buildModeName),
202 203
        if (install)
          ...<String>['--target', 'INSTALL'],
204
        if (globals.logger.isVerbose)
205
          '--verbose',
206 207 208
      ],
      environment: <String, String>{
        if (globals.logger.isVerbose)
209
          'VERBOSE_SCRIPT_LOGGING': 'true',
210 211
      },
      trace: true,
212
      stdoutErrorMatcher: errorMatcher,
213
    );
214
  } on ArgumentError {
215
    throwToolExit("cmake not found. Run 'flutter doctor' for more information.");
216 217
  }
  if (result != 0) {
218
    throwToolExit('Build process failed.');
219
  }
220
  globals.flutterUsage.sendTiming('build', 'windows-cmake-build', Duration(milliseconds: sw.elapsedMilliseconds));
221
}
222

223 224
/// Writes the generated CMake file with the configuration for the given build.
void _writeGeneratedFlutterConfig(
225 226
  WindowsProject windowsProject,
  BuildInfo buildInfo,
227
  String? target,
228
) {
229
  final Map<String, String> environment = <String, String>{
230
    'FLUTTER_ROOT': Cache.flutterRoot!,
231
    'FLUTTER_EPHEMERAL_DIR': windowsProject.ephemeralDirectory.path,
232
    'PROJECT_DIR': windowsProject.parent.directory.path,
233 234
    if (target != null)
      'FLUTTER_TARGET': target,
235
    ...buildInfo.toEnvironmentConfig(),
236
  };
237 238 239
  final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo;
  if (localEngineInfo != null) {
    final String engineOutPath = localEngineInfo.engineOutPath;
240
    environment['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath));
241
    environment['LOCAL_ENGINE'] = localEngineInfo.localEngineName;
242
  }
243
  writeGeneratedCmakeConfig(Cache.flutterRoot!, windowsProject, buildInfo, environment);
244
}
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296

// Works around the Visual Studio 17.1.0 CMake bug described in
// https://github.com/flutter/flutter/issues/97086
//
// Rather than attempt to remove all the duplicate entries within the
// <CustomBuild> element, which would require a more complicated parser, this
// just fixes the incorrect duplicates to have the correct `$<CONFIG>` value,
// making the duplication harmless.
//
// TODO(stuartmorgan): Remove this workaround either once 17.1.0 is
// sufficiently old that we no longer need to support it, or when
// dropping VS 2022 support.
void _fixBrokenCmakeGeneration(Directory buildDirectory) {
  final File assembleProject = buildDirectory
    .childDirectory('flutter')
    .childFile('flutter_assemble.vcxproj');
  if (assembleProject.existsSync()) {
    // E.g.: <Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    final RegExp commandRegex = RegExp(
      r'<Command Condition=.*\(Configuration\)\|\$\(Platform\).==.(Debug|Profile|Release)\|');
    // E.g.: [...]/flutter_tools/bin/tool_backend.bat windows-x64 Debug
    final RegExp assembleCallRegex = RegExp(
      r'^.*/tool_backend\.bat windows[^ ]* (Debug|Profile|Release)');
    String? lastCommandConditionConfig;
    final StringBuffer newProjectContents = StringBuffer();
    // vcxproj files contain a BOM, which readAsLinesSync drops; re-add it.
    newProjectContents.writeCharCode(unicodeBomCharacterRune);
    for (final String line in assembleProject.readAsLinesSync()) {
      final RegExpMatch? commandMatch = commandRegex.firstMatch(line);
      if (commandMatch != null) {
        lastCommandConditionConfig = commandMatch.group(1);
      } else if (lastCommandConditionConfig != null) {
        final RegExpMatch? assembleCallMatch = assembleCallRegex.firstMatch(line);
        if (assembleCallMatch != null) {
          final String callConfig = assembleCallMatch.group(1)!;
          if (callConfig != lastCommandConditionConfig) {
            // The config is the end of the line; make sure to replace that one,
            // in case config-matching strings appear anywhere else in the line
            // (e.g., the project path).
            final int badConfigIndex = line.lastIndexOf(assembleCallMatch.group(1)!);
            final String correctedLine = line.replaceFirst(
              callConfig, lastCommandConditionConfig, badConfigIndex);
            newProjectContents.writeln('$correctedLine\r');
            continue;
          }
        }
      }
      newProjectContents.writeln('$line\r');
    }
    assembleProject.writeAsStringSync(newProjectContents.toString());
  }
}