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

5
import 'package:package_config/package_config_types.dart';
6

7
import 'artifacts.dart';
8
import 'base/config.dart';
9
import 'base/context.dart';
10
import 'base/file_system.dart';
11
import 'base/logger.dart';
12
import 'base/os.dart';
13
import 'base/utils.dart';
14
import 'convert.dart';
15
import 'globals.dart' as globals;
16
import 'web/compile.dart';
17

18 19 20
/// Whether icon font subsetting is enabled by default.
const bool kIconTreeShakerEnabledDefault = true;

21 22
/// Information about a build to be performed or used.
class BuildInfo {
23 24 25
  const BuildInfo(
    this.mode,
    this.flavor, {
26
    this.trackWidgetCreation = false,
27 28
    List<String>? extraFrontEndOptions,
    List<String>? extraGenSnapshotOptions,
29
    List<String>? fileSystemRoots,
30
    this.androidProjectArgs = const <String>[],
31
    this.fileSystemScheme,
32 33
    this.buildNumber,
    this.buildName,
34
    this.splitDebugInfoPath,
35
    this.dartObfuscation = false,
36
    List<String>? dartDefines,
37
    this.bundleSkSLPath,
38
    List<String>? dartExperiments,
39
    this.webRenderer = WebRendererMode.autoDetect,
40
    required this.treeShakeIcons,
41
    this.performanceMeasurementFile,
42
    this.dartDefineConfigJsonMap,
43
    this.packagesPath = '.dart_tool/package_config.json', // TODO(zanderso): make this required and remove the default.
44
    this.nullSafetyMode = NullSafetyMode.sound,
45
    this.codeSizeDirectory,
46
    this.androidGradleDaemon = true,
47
    this.packageConfig = PackageConfig.empty,
48
    this.initializeFromDill,
49
    this.assumeInitializeFromDillUpToDate = false,
50 51
  }) : extraFrontEndOptions = extraFrontEndOptions ?? const <String>[],
       extraGenSnapshotOptions = extraGenSnapshotOptions ?? const <String>[],
52
       fileSystemRoots = fileSystemRoots ?? const <String>[],
53 54
       dartDefines = dartDefines ?? const <String>[],
       dartExperiments = dartExperiments ?? const <String>[];
55 56

  final BuildMode mode;
57

58 59 60 61 62
  /// The null safety mode the application should be run in.
  ///
  /// If not provided, defaults to [NullSafetyMode.autodetect].
  final NullSafetyMode nullSafetyMode;

63
  /// Whether the build should subset icon fonts.
64 65
  final bool treeShakeIcons;

66 67 68 69 70 71
  /// Represents a custom Android product flavor or an Xcode scheme, null for
  /// using the default.
  ///
  /// If not null, the Gradle build task will be `assembleFlavorMode` (e.g.
  /// `assemblePaidRelease`), and the Xcode build configuration will be
  /// Mode-Flavor (e.g. Release-Paid).
72
  final String? flavor;
73

74
  /// The path to the package configuration file to use for compilation.
75 76
  ///
  /// This is used by package:package_config to locate the actual package_config.json
77
  /// file. If not provided, defaults to `.dart_tool/package_config.json`.
78 79
  final String packagesPath;

80
  final List<String> fileSystemRoots;
81
  final String? fileSystemScheme;
82

83 84 85
  /// Whether the build should track widget creation locations.
  final bool trackWidgetCreation;

86
  /// Extra command-line options for front-end.
87
  final List<String> extraFrontEndOptions;
88 89

  /// Extra command-line options for gen_snapshot.
90
  final List<String> extraGenSnapshotOptions;
91

92 93 94 95 96
  /// Internal version number (not displayed to users).
  /// Each build must have a unique number to differentiate it from previous builds.
  /// It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.
  /// On Android it is used as versionCode.
  /// On Xcode builds it is used as CFBundleVersion.
97
  /// On Windows it is used as the build suffix for the product and file versions.
98
  final String? buildNumber;
99 100 101 102

  /// A "x.y.z" string used as the version number shown to users.
  /// For each new version of your app, you will provide a version number to differentiate it from previous versions.
  /// On Android it is used as versionName.
103
  /// On Xcode builds it is used as CFBundleShortVersionString.
104
  /// On Windows it is used as the major, minor, and patch parts of the product and file versions.
105
  final String? buildName;
106

107 108 109
  /// An optional directory path to save debugging information from dwarf stack
  /// traces. If null, stack trace information is not stripped from the
  /// executable.
110
  final String? splitDebugInfoPath;
111

112 113 114
  /// Whether to apply dart source code obfuscation.
  final bool dartObfuscation;

115
  /// An optional path to a JSON containing object SkSL shaders.
116 117
  ///
  /// Currently this is only supported for Android builds.
118
  final String? bundleSkSLPath;
119

120 121 122 123 124 125
  /// Additional constant values to be made available in the Dart program.
  ///
  /// These values can be used with the const `fromEnvironment` constructors of
  /// [bool], [String], [int], and [double].
  final List<String> dartDefines;

126 127 128
  /// A list of Dart experiments.
  final List<String> dartExperiments;

129 130 131
  /// When compiling to web, which web renderer mode we are using (html, canvaskit, auto)
  final WebRendererMode webRenderer;

132 133 134 135 136
  /// The name of a file where flutter assemble will output performance
  /// information in a JSON format.
  ///
  /// This is not considered a build input and will not force assemble to
  /// rerun tasks.
137
  final String? performanceMeasurementFile;
138

139 140 141 142 143 144 145 146 147 148 149
  /// Configure a constant pool file.
  /// Additional constant values to be made available in the Dart program.
  ///
  /// These values can be used with the const `fromEnvironment` constructors of
  ///  [String] the key and field are json values
  /// json value
  ///
  /// An additional field `dartDefineConfigJsonMap` is provided to represent the native JSON value of the configuration file
  ///
  final Map<String, Object>? dartDefineConfigJsonMap;

150
  /// If provided, an output directory where one or more v8-style heap snapshots
151
  /// will be written for code size profiling.
152
  final String? codeSizeDirectory;
153

154 155 156 157 158 159 160 161 162 163 164 165 166 167
  /// Whether to enable the Gradle daemon when performing an Android build.
  ///
  /// Starting the daemon is the default behavior of the gradle wrapper script created
  /// in a Flutter project. Setting this value to false will cause the tool to pass
  /// `--no-daemon` to the gradle wrapper script, preventing it from spawning a daemon
  /// process.
  ///
  /// For one-off builds or CI systems, preventing the daemon from spawning will
  /// reduce system resource usage, at the cost of any subsequent builds starting
  /// up slightly slower.
  ///
  /// The Gradle daemon may also be disabled in the Android application's properties file.
  final bool androidGradleDaemon;

168 169 170 171
  /// Additional key value pairs that are passed directly to the gradle project via the `-P`
  /// flag.
  final List<String> androidProjectArgs;

172 173 174 175 176 177
  /// The package configuration for the loaded application.
  ///
  /// This is captured once during startup, but the actual package configuration
  /// may change during a 'flutter run` workflow.
  final PackageConfig packageConfig;

178 179 180 181 182
  /// The kernel file that the resident compiler will be initialized with.
  ///
  /// If this is null, it will be initialized from the default cached location.
  final String? initializeFromDill;

183 184 185 186
  /// If set, assumes that the file passed in [initializeFromDill] is up to date
  /// and skips the check and potential invalidation of files.
  final bool assumeInitializeFromDillUpToDate;

187
  static const BuildInfo debug = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, treeShakeIcons: false);
188 189 190
  static const BuildInfo profile = BuildInfo(BuildMode.profile, null, treeShakeIcons: kIconTreeShakerEnabledDefault);
  static const BuildInfo jitRelease = BuildInfo(BuildMode.jitRelease, null, treeShakeIcons: kIconTreeShakerEnabledDefault);
  static const BuildInfo release = BuildInfo(BuildMode.release, null, treeShakeIcons: kIconTreeShakerEnabledDefault);
191 192 193 194 195 196 197 198

  /// Returns whether a debug build is requested.
  ///
  /// Exactly one of [isDebug], [isProfile], or [isRelease] is true.
  bool get isDebug => mode == BuildMode.debug;

  /// Returns whether a profile build is requested.
  ///
199 200
  /// Exactly one of [isDebug], [isProfile], [isJitRelease],
  /// or [isRelease] is true.
201
  bool get isProfile => mode == BuildMode.profile;
202 203 204

  /// Returns whether a release build is requested.
  ///
205 206
  /// Exactly one of [isDebug], [isProfile], [isJitRelease],
  /// or [isRelease] is true.
207
  bool get isRelease => mode == BuildMode.release;
208

209 210 211 212 213 214
  /// Returns whether a JIT release build is requested.
  ///
  /// Exactly one of [isDebug], [isProfile], [isJitRelease],
  /// or [isRelease] is true.
  bool get isJitRelease => mode == BuildMode.jitRelease;

215 216 217 218
  bool get usesAot => isAotBuildMode(mode);
  bool get supportsEmulator => isEmulatorBuildMode(mode);
  bool get supportsSimulator => isEmulatorBuildMode(mode);
  String get modeName => getModeName(mode);
219
  String get friendlyModeName => getFriendlyModeName(mode);
220

221
  /// the flavor name in the output apk files is lower-cased (see flutter.gradle),
222
  /// so the lower cased flavor name is used to compute the output file name
223
  String? get lowerCasedFlavor => flavor?.toLowerCase();
224

225 226 227 228
  /// the flavor name in the output bundle files has the first character lower-cased,
  /// so the uncapitalized flavor name is used to compute the output file name
  String? get uncapitalizedFlavor => _uncapitalize(flavor);

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
  /// Convert to a structured string encoded structure appropriate for usage
  /// in build system [Environment.defines].
  ///
  /// Fields that are `null` are excluded from this configuration.
  Map<String, String> toBuildSystemEnvironment() {
    // packagesPath and performanceMeasurementFile are not passed into
    // the Environment map.
    return <String, String>{
      kBuildMode: getNameForBuildMode(mode),
      if (dartDefines.isNotEmpty)
        kDartDefines: encodeDartDefines(dartDefines),
      if (dartObfuscation != null)
        kDartObfuscation: dartObfuscation.toString(),
      if (extraFrontEndOptions.isNotEmpty)
        kExtraFrontEndOptions: extraFrontEndOptions.join(','),
      if (extraGenSnapshotOptions.isNotEmpty)
        kExtraGenSnapshotOptions: extraGenSnapshotOptions.join(','),
      if (splitDebugInfoPath != null)
        kSplitDebugInfo: splitDebugInfoPath!,
      if (trackWidgetCreation != null)
        kTrackWidgetCreation: trackWidgetCreation.toString(),
      if (treeShakeIcons != null)
        kIconTreeShakerFlag: treeShakeIcons.toString(),
      if (bundleSkSLPath != null)
        kBundleSkSLPath: bundleSkSLPath!,
      if (codeSizeDirectory != null)
        kCodeSizeDirectory: codeSizeDirectory!,
      if (fileSystemRoots.isNotEmpty)
        kFileSystemRoots: fileSystemRoots.join(','),
      if (fileSystemScheme != null)
        kFileSystemScheme: fileSystemScheme!,
260 261 262 263
      if (buildName != null)
        kBuildName: buildName!,
      if (buildNumber != null)
        kBuildNumber: buildNumber!,
264 265 266
    };
  }

267

268
  /// Convert to a structured string encoded structure appropriate for usage as
269 270
  /// environment variables or to embed in other scripts.
  ///
271
  /// Fields that are `null` are excluded from this configuration.
272
  Map<String, String> toEnvironmentConfig() {
273 274 275 276 277
    final Map<String, String> map = <String, String>{};
    dartDefineConfigJsonMap?.forEach((String key, Object value) {
      map[key] = '$value';
    });
    final Map<String, String> environmentMap = <String, String>{
278
      if (dartDefines.isNotEmpty)
279
        'DART_DEFINES': encodeDartDefines(dartDefines),
280 281
      if (dartObfuscation != null)
        'DART_OBFUSCATION': dartObfuscation.toString(),
282 283 284 285
      if (extraFrontEndOptions.isNotEmpty)
        'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions.join(','),
      if (extraGenSnapshotOptions.isNotEmpty)
        'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions.join(','),
286
      if (splitDebugInfoPath != null)
287
        'SPLIT_DEBUG_INFO': splitDebugInfoPath!,
288 289 290 291
      if (trackWidgetCreation != null)
        'TRACK_WIDGET_CREATION': trackWidgetCreation.toString(),
      if (treeShakeIcons != null)
        'TREE_SHAKE_ICONS': treeShakeIcons.toString(),
292
      if (performanceMeasurementFile != null)
293
        'PERFORMANCE_MEASUREMENT_FILE': performanceMeasurementFile!,
294
      if (bundleSkSLPath != null)
295
        'BUNDLE_SKSL_PATH': bundleSkSLPath!,
296 297
      if (packagesPath != null)
        'PACKAGE_CONFIG': packagesPath,
298
      if (codeSizeDirectory != null)
299
        'CODE_SIZE_DIRECTORY': codeSizeDirectory!,
300
    };
301 302 303 304 305 306 307 308 309 310
    map.forEach((String key, String value) {
      if (environmentMap.containsKey(key)) {
        globals.printWarning(
            'The key: [$key] already exists, you cannot use environment variables that have been used by the system!');
      } else {
        // System priority is greater than user priority
        environmentMap[key] = value;
      }
    });
    return environmentMap;
311
  }
312 313 314 315 316

  /// Convert this config to a series of project level arguments to be passed
  /// on the command line to gradle.
  List<String> toGradleConfig() {
    // PACKAGE_CONFIG not currently supported.
317
    final List<String> result = <String>[
318
      if (dartDefines.isNotEmpty)
319 320 321
        '-Pdart-defines=${encodeDartDefines(dartDefines)}',
      if (dartObfuscation != null)
        '-Pdart-obfuscation=$dartObfuscation',
322 323 324 325
      if (extraFrontEndOptions.isNotEmpty)
        '-Pextra-front-end-options=${extraFrontEndOptions.join(',')}',
      if (extraGenSnapshotOptions.isNotEmpty)
        '-Pextra-gen-snapshot-options=${extraGenSnapshotOptions.join(',')}',
326 327 328 329 330 331 332 333 334 335 336 337
      if (splitDebugInfoPath != null)
        '-Psplit-debug-info=$splitDebugInfoPath',
      if (trackWidgetCreation != null)
        '-Ptrack-widget-creation=$trackWidgetCreation',
      if (treeShakeIcons != null)
        '-Ptree-shake-icons=$treeShakeIcons',
      if (performanceMeasurementFile != null)
        '-Pperformance-measurement-file=$performanceMeasurementFile',
      if (bundleSkSLPath != null)
        '-Pbundle-sksl-path=$bundleSkSLPath',
      if (codeSizeDirectory != null)
        '-Pcode-size-directory=$codeSizeDirectory',
338 339
      for (String projectArg in androidProjectArgs)
        '-P$projectArg',
340
    ];
341 342 343 344
    if (dartDefineConfigJsonMap != null) {
      final Iterable<String> gradleConfKeys = result.map((final String gradleConf) => gradleConf.split('=')[0].substring(2));
      dartDefineConfigJsonMap!.forEach((String key, Object value) {
        if (gradleConfKeys.contains(key)) {
345 346 347
          globals.printWarning(
              'The key: [$key] already exists, you cannot use gradle variables that have been used by the system!');
        } else {
348
          result.add('-P$key=$value');
349
        }
350
      });
351 352
    }
    return result;
353
  }
354 355 356 357 358 359 360 361 362
}

/// Information about an Android build to be performed or used.
class AndroidBuildInfo {
  const AndroidBuildInfo(
    this.buildInfo, {
    this.targetArchs = const <AndroidArch>[
      AndroidArch.armeabi_v7a,
      AndroidArch.arm64_v8a,
363
      AndroidArch.x86_64,
364 365
    ],
    this.splitPerAbi = false,
366
    this.fastStart = false,
367
    this.multidexEnabled = false,
368
  });
369

370 371 372 373 374 375 376 377 378 379 380 381
  // The build info containing the mode and flavor.
  final BuildInfo buildInfo;

  /// Whether to split the shared library per ABI.
  ///
  /// When this is false, multiple ABIs will be contained within one primary
  /// build artifact. When this is true, multiple build artifacts (one per ABI)
  /// will be produced.
  final bool splitPerAbi;

  /// The target platforms for the build.
  final Iterable<AndroidArch> targetArchs;
382 383 384

  /// Whether to bootstrap an empty application.
  final bool fastStart;
385 386 387

  /// Whether to enable multidex support for apps with more than 64k methods.
  final bool multidexEnabled;
388 389
}

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
/// A summary of the compilation strategy used for Dart.
class BuildMode {
  const BuildMode._(this.name);

  factory BuildMode.fromName(String value) {
    switch (value) {
      case 'debug':
        return BuildMode.debug;
      case 'profile':
        return BuildMode.profile;
      case 'release':
        return BuildMode.release;
      case 'jit_release':
        return BuildMode.jitRelease;
    }
    throw ArgumentError('$value is not a supported build mode');
  }

  /// Built in JIT mode with no optimizations, enabled asserts, and an observatory.
  static const BuildMode debug = BuildMode._('debug');

  /// Built in AOT mode with some optimizations and an observatory.
  static const BuildMode profile = BuildMode._('profile');

  /// Built in AOT mode with all optimizations and no observatory.
  static const BuildMode release = BuildMode._('release');

  /// Built in JIT mode with all optimizations and no observatory.
  static const BuildMode jitRelease = BuildMode._('jit_release');

  static const List<BuildMode> values = <BuildMode>[
    debug,
    profile,
    release,
    jitRelease,
  ];
  static const Set<BuildMode> releaseModes = <BuildMode>{
    release,
    jitRelease,
  };
  static const Set<BuildMode> jitModes = <BuildMode>{
    debug,
    jitRelease,
  };

  /// Whether this mode is considered release.
  ///
  /// Useful for determining whether we should enable/disable asserts or
  /// other development features.
  bool get isRelease => releaseModes.contains(this);
440

441
  /// Whether this mode is using the JIT runtime.
442 443 444 445 446 447 448 449 450 451 452
  bool get isJit => jitModes.contains(this);

  /// Whether this mode is using the precompiled runtime.
  bool get isPrecompiled => !isJit;

  /// The name for this build mode.
  final String name;

  @override
  String toString() => name;
}
453 454 455

/// Return the name for the build mode, or "any" if null.
String getNameForBuildMode(BuildMode buildMode) {
456
  return buildMode.name;
457 458 459 460
}

/// Returns the [BuildMode] for a particular `name`.
BuildMode getBuildModeForName(String name) {
461
  return BuildMode.fromName(name);
462 463
}

464 465 466 467 468 469
/// Environment type of the target device.
enum EnvironmentType {
  physical,
  simulator,
}

470
String? validatedBuildNumberForPlatform(TargetPlatform targetPlatform, String? buildNumber, Logger logger) {
471 472 473 474
  if (buildNumber == null) {
    return null;
  }
  if (targetPlatform == TargetPlatform.ios ||
475
      targetPlatform == TargetPlatform.darwin) {
476 477 478
    // See CFBundleVersion at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
    final RegExp disallowed = RegExp(r'[^\d\.]');
    String tmpBuildNumber = buildNumber.replaceAll(disallowed, '');
479 480 481
    if (tmpBuildNumber.isEmpty) {
      return null;
    }
482 483 484 485 486 487 488 489 490
    final List<String> segments = tmpBuildNumber
        .split('.')
        .where((String segment) => segment.isNotEmpty)
        .toList();
    if (segments.isEmpty) {
      segments.add('0');
    }
    tmpBuildNumber = segments.join('.');
    if (tmpBuildNumber != buildNumber) {
491
      logger.printTrace('Invalid build-number: $buildNumber for iOS/macOS, overridden by $tmpBuildNumber.\n'
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508
          'See CFBundleVersion at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html');
    }
    return tmpBuildNumber;
  }
  if (targetPlatform == TargetPlatform.android_arm ||
      targetPlatform == TargetPlatform.android_arm64 ||
      targetPlatform == TargetPlatform.android_x64 ||
      targetPlatform == TargetPlatform.android_x86) {
    // See versionCode at https://developer.android.com/studio/publish/versioning
    final RegExp disallowed = RegExp(r'[^\d]');
    String tmpBuildNumberStr = buildNumber.replaceAll(disallowed, '');
    int tmpBuildNumberInt = int.tryParse(tmpBuildNumberStr) ?? 0;
    if (tmpBuildNumberInt < 1) {
      tmpBuildNumberInt = 1;
    }
    tmpBuildNumberStr = tmpBuildNumberInt.toString();
    if (tmpBuildNumberStr != buildNumber) {
509
      logger.printTrace('Invalid build-number: $buildNumber for Android, overridden by $tmpBuildNumberStr.\n'
510 511 512 513 514 515 516
          'See versionCode at https://developer.android.com/studio/publish/versioning');
    }
    return tmpBuildNumberStr;
  }
  return buildNumber;
}

517
String? validatedBuildNameForPlatform(TargetPlatform targetPlatform, String? buildName, Logger logger) {
518 519 520 521
  if (buildName == null) {
    return null;
  }
  if (targetPlatform == TargetPlatform.ios ||
522
      targetPlatform == TargetPlatform.darwin) {
523 524 525
    // See CFBundleShortVersionString at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
    final RegExp disallowed = RegExp(r'[^\d\.]');
    String tmpBuildName = buildName.replaceAll(disallowed, '');
526 527 528
    if (tmpBuildName.isEmpty) {
      return null;
    }
529 530 531 532 533 534 535 536 537
    final List<String> segments = tmpBuildName
        .split('.')
        .where((String segment) => segment.isNotEmpty)
        .toList();
    while (segments.length < 3) {
      segments.add('0');
    }
    tmpBuildName = segments.join('.');
    if (tmpBuildName != buildName) {
538
      logger.printTrace('Invalid build-name: $buildName for iOS/macOS, overridden by $tmpBuildName.\n'
539 540 541 542
          'See CFBundleShortVersionString at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html');
    }
    return tmpBuildName;
  }
543 544
  if (targetPlatform == TargetPlatform.android ||
      targetPlatform == TargetPlatform.android_arm ||
545 546 547 548 549 550 551 552 553
      targetPlatform == TargetPlatform.android_arm64 ||
      targetPlatform == TargetPlatform.android_x64 ||
      targetPlatform == TargetPlatform.android_x86) {
    // See versionName at https://developer.android.com/studio/publish/versioning
    return buildName;
  }
  return buildName;
}

554
String getModeName(BuildMode mode) => getEnumName(mode);
555

556 557 558 559
String getFriendlyModeName(BuildMode mode) {
  return snakeCase(getModeName(mode)).replaceAll('_', ' ');
}

560 561 562 563 564
// Returns true if the selected build mode uses ahead-of-time compilation.
bool isAotBuildMode(BuildMode mode) {
  return mode == BuildMode.profile || mode == BuildMode.release;
}

565
// Returns true if the given build mode can be used on emulators / simulators.
566
bool isEmulatorBuildMode(BuildMode mode) {
567
  return mode == BuildMode.debug;
568
}
569

570
enum TargetPlatform {
571
  android,
572
  ios,
573
  darwin,
574
  linux_x64,
575
  linux_arm64,
576
  windows_x64,
577 578
  fuchsia_arm64,
  fuchsia_x64,
579
  tester,
580
  web_javascript,
581
  // The arch specific android target platforms are soft-deprecated.
582 583 584 585 586 587 588
  // Instead of using TargetPlatform as a combination arch + platform
  // the code will be updated to carry arch information in [DarwinArch]
  // and [AndroidArch].
  android_arm,
  android_arm64,
  android_x64,
  android_x86,
589 590
}

591
/// iOS and macOS target device architecture.
592 593
//
// TODO(cbracken): split TargetPlatform.ios into ios_armv7, ios_arm64.
594
enum DarwinArch {
595
  armv7, // Deprecated. Used to display 32-bit unsupported devices.
596
  arm64,
597
  x86_64,
598 599
}

600
// TODO(zanderso): replace all android TargetPlatform usage with AndroidArch.
601 602 603 604 605 606 607
enum AndroidArch {
  armeabi_v7a,
  arm64_v8a,
  x86,
  x86_64,
}

608
/// The default set of iOS device architectures to build for.
609
List<DarwinArch> defaultIOSArchsForEnvironment(
610 611 612 613
  EnvironmentType environmentType,
  Artifacts artifacts,
) {
  // Handle single-arch local engines.
614 615 616
  final LocalEngineInfo? localEngineInfo = artifacts.localEngineInfo;
  if (localEngineInfo != null) {
    final String localEngineName = localEngineInfo.localEngineName;
617 618 619 620 621 622 623
    if (localEngineName.contains('_arm64')) {
      return <DarwinArch>[ DarwinArch.arm64 ];
    }
    if (localEngineName.contains('_sim')) {
      return <DarwinArch>[ DarwinArch.x86_64 ];
    }
  } else if (environmentType == EnvironmentType.simulator) {
624 625
    return <DarwinArch>[
      DarwinArch.x86_64,
626
      DarwinArch.arm64,
627
    ];
628
  }
629 630 631
  return <DarwinArch>[
    DarwinArch.arm64,
  ];
632
}
633

634 635 636
/// The default set of macOS device architectures to build for.
List<DarwinArch> defaultMacOSArchsForEnvironment(Artifacts artifacts) {
  // Handle single-arch local engines.
637 638 639
  final LocalEngineInfo? localEngineInfo = artifacts.localEngineInfo;
  if (localEngineInfo != null) {
    if (localEngineInfo.localEngineName.contains('_arm64')) {
640 641 642 643 644 645 646 647 648 649
      return <DarwinArch>[ DarwinArch.arm64 ];
    }
    return <DarwinArch>[ DarwinArch.x86_64 ];
  }
  return <DarwinArch>[
    DarwinArch.x86_64,
    DarwinArch.arm64,
  ];
}

650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669
// Returns the Dart SDK's name for the specified target architecture.
//
// When building for Darwin platforms, the tool invokes architecture-specific
// variants of `gen_snapshot`, one for each target architecture. The output
// instructions are then built into architecture-specific binaries, which are
// merged into a universal binary using the `lipo` tool.
String getDartNameForDarwinArch(DarwinArch arch) {
  switch (arch) {
    case DarwinArch.armv7:
      return 'armv7';
    case DarwinArch.arm64:
      return 'arm64';
    case DarwinArch.x86_64:
      return 'x64';
  }
}

// Returns Apple's name for the specified target architecture.
//
// When invoking Apple tools such as `xcodebuild` or `lipo`, the tool often
eggfly's avatar
eggfly committed
670
// passes one or more target architectures as parameters. The names returned by
671 672 673 674
// this function reflect Apple's name for the specified architecture.
//
// For consistency with developer expectations, Flutter outputs also use these
// architecture names in its build products for Darwin target platforms.
675
String getNameForDarwinArch(DarwinArch arch) {
676
  switch (arch) {
677
    case DarwinArch.armv7:
678
      return 'armv7';
679
    case DarwinArch.arm64:
680
      return 'arm64';
681 682
    case DarwinArch.x86_64:
      return 'x86_64';
683 684 685
  }
}

686
DarwinArch getIOSArchForName(String arch) {
687 688
  switch (arch) {
    case 'armv7':
689
    case 'armv7f': // iPhone 4S.
690
    case 'armv7s': // iPad 4.
691
      return DarwinArch.armv7;
692
    case 'arm64':
693
    case 'arm64e': // iPhone XS/XS Max/XR and higher. arm64 runs on arm64e devices.
694
      return DarwinArch.arm64;
695 696
    case 'x86_64':
      return DarwinArch.x86_64;
697
  }
698
  throw Exception('Unsupported iOS arch name "$arch"');
699 700
}

701 702 703 704 705 706 707 708 709 710
DarwinArch getDarwinArchForName(String arch) {
  switch (arch) {
    case 'arm64':
      return DarwinArch.arm64;
    case 'x86_64':
      return DarwinArch.x86_64;
  }
  throw Exception('Unsupported MacOS arch name "$arch"');
}

711
String getNameForTargetPlatform(TargetPlatform platform, {DarwinArch? darwinArch}) {
712 713
  switch (platform) {
    case TargetPlatform.android_arm:
714
      return 'android-arm';
715 716
    case TargetPlatform.android_arm64:
      return 'android-arm64';
717
    case TargetPlatform.android_x64:
718
      return 'android-x64';
719
    case TargetPlatform.android_x86:
720
      return 'android-x86';
721
    case TargetPlatform.ios:
722 723 724
      if (darwinArch != null) {
        return 'ios-${getNameForDarwinArch(darwinArch)}';
      }
725
      return 'ios';
726 727 728 729 730
    case TargetPlatform.darwin:
      if (darwinArch != null) {
        return 'darwin-${getNameForDarwinArch(darwinArch)}';
      }
      return 'darwin';
731
    case TargetPlatform.linux_x64:
732
      return 'linux-x64';
733
    case TargetPlatform.linux_arm64:
734
      return 'linux-arm64';
735
    case TargetPlatform.windows_x64:
736
      return 'windows-x64';
737 738 739 740
    case TargetPlatform.fuchsia_arm64:
      return 'fuchsia-arm64';
    case TargetPlatform.fuchsia_x64:
      return 'fuchsia-x64';
741 742
    case TargetPlatform.tester:
      return 'flutter-tester';
743 744
    case TargetPlatform.web_javascript:
      return 'web-javascript';
745 746
    case TargetPlatform.android:
      return 'android';
747
  }
748 749
}

750
TargetPlatform getTargetPlatformForName(String platform) {
751
  switch (platform) {
752 753
    case 'android':
      return TargetPlatform.android;
754 755
    case 'android-arm':
      return TargetPlatform.android_arm;
756 757
    case 'android-arm64':
      return TargetPlatform.android_arm64;
758 759 760 761
    case 'android-x64':
      return TargetPlatform.android_x64;
    case 'android-x86':
      return TargetPlatform.android_x86;
762 763 764 765
    case 'fuchsia-arm64':
      return TargetPlatform.fuchsia_arm64;
    case 'fuchsia-x64':
      return TargetPlatform.fuchsia_x64;
766 767
    case 'ios':
      return TargetPlatform.ios;
768 769 770
    case 'darwin':
    // For backward-compatibility and also for Tester, where it must match
    // host platform name (HostPlatform.darwin_x64)
771
    case 'darwin-x64':
772
    case 'darwin-arm64':
773
      return TargetPlatform.darwin;
774
    case 'linux-x64':
775
      return TargetPlatform.linux_x64;
776 777
   case 'linux-arm64':
      return TargetPlatform.linux_arm64;
778
    case 'windows-x64':
779
      return TargetPlatform.windows_x64;
780 781
    case 'web-javascript':
      return TargetPlatform.web_javascript;
782
  }
783
  throw Exception('Unsupported platform name "$platform"');
784 785
}

786 787 788 789 790 791 792 793 794 795 796
AndroidArch getAndroidArchForName(String platform) {
  switch (platform) {
    case 'android-arm':
      return AndroidArch.armeabi_v7a;
    case 'android-arm64':
      return AndroidArch.arm64_v8a;
    case 'android-x64':
      return AndroidArch.x86_64;
    case 'android-x86':
      return AndroidArch.x86;
  }
797
  throw Exception('Unsupported Android arch name "$platform"');
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
}

String getNameForAndroidArch(AndroidArch arch) {
  switch (arch) {
    case AndroidArch.armeabi_v7a:
      return 'armeabi-v7a';
    case AndroidArch.arm64_v8a:
      return 'arm64-v8a';
    case AndroidArch.x86_64:
      return 'x86_64';
    case AndroidArch.x86:
      return 'x86';
  }
}

String getPlatformNameForAndroidArch(AndroidArch arch) {
  switch (arch) {
    case AndroidArch.armeabi_v7a:
      return 'android-arm';
    case AndroidArch.arm64_v8a:
      return 'android-arm64';
    case AndroidArch.x86_64:
      return 'android-x64';
    case AndroidArch.x86:
      return 'android-x86';
  }
}

826 827 828 829 830 831
String fuchsiaArchForTargetPlatform(TargetPlatform targetPlatform) {
  switch (targetPlatform) {
    case TargetPlatform.fuchsia_arm64:
      return 'arm64';
    case TargetPlatform.fuchsia_x64:
      return 'x64';
832 833 834 835 836 837 838 839 840 841 842 843
    case TargetPlatform.android:
    case TargetPlatform.android_arm:
    case TargetPlatform.android_arm64:
    case TargetPlatform.android_x64:
    case TargetPlatform.android_x86:
    case TargetPlatform.darwin:
    case TargetPlatform.ios:
    case TargetPlatform.linux_arm64:
    case TargetPlatform.linux_x64:
    case TargetPlatform.tester:
    case TargetPlatform.web_javascript:
    case TargetPlatform.windows_x64:
844
      throw UnsupportedError('Unexpected Fuchsia platform $targetPlatform');
845 846 847
  }
}

848
HostPlatform getCurrentHostPlatform() {
849
  if (globals.platform.isMacOS) {
850
    return HostPlatform.darwin_x64;
851
  }
852
  if (globals.platform.isLinux) {
853 854
    // support x64 and arm64 architecture.
    return globals.os.hostPlatform;
855
  }
856
  if (globals.platform.isWindows) {
857
    return HostPlatform.windows_x64;
858
  }
859

860
  globals.printWarning('Unsupported host platform, defaulting to Linux');
861 862

  return HostPlatform.linux_x64;
863
}
864

865 866 867 868
FileSystemEntity getWebPlatformBinariesDirectory(Artifacts artifacts, WebRendererMode webRenderer) {
  return artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder);
}

869
/// Returns the top-level build output directory.
870
String getBuildDirectory([Config? config, FileSystem? fileSystem]) {
871 872
  // TODO(johnmccutchan): Stop calling this function as part of setting
  // up command line argument processing.
873 874 875 876 877 878
  if (context == null) {
    return 'build';
  }
  final Config localConfig = config ?? globals.config;
  final FileSystem localFilesystem = fileSystem ?? globals.fs;
  if (localConfig == null) {
879
    return 'build';
880
  }
881

882
  final String buildDir = localConfig.getValue('build-dir') as String? ?? 'build';
883
  if (localFilesystem.path.isAbsolute(buildDir)) {
884
    throw Exception(
885
        'build-dir config setting in ${globals.config.configPath} must be relative');
886 887 888 889 890 891
  }
  return buildDir;
}

/// Returns the Android build output directory.
String getAndroidBuildDirectory() {
892
  // TODO(cbracken): move to android subdir.
893 894 895 896 897
  return getBuildDirectory();
}

/// Returns the AOT build output directory.
String getAotBuildDirectory() {
898
  return globals.fs.path.join(getBuildDirectory(), 'aot');
899 900 901 902
}

/// Returns the asset build output directory.
String getAssetBuildDirectory() {
903
  return globals.fs.path.join(getBuildDirectory(), 'flutter_assets');
904 905 906 907
}

/// Returns the iOS build output directory.
String getIosBuildDirectory() {
908
  return globals.fs.path.join(getBuildDirectory(), 'ios');
909
}
910

911 912
/// Returns the macOS build output directory.
String getMacOSBuildDirectory() {
913
  return globals.fs.path.join(getBuildDirectory(), 'macos');
914 915
}

916
/// Returns the web build output directory.
917 918
String getWebBuildDirectory([bool isWasm = false]) {
  return globals.fs.path.join(getBuildDirectory(), isWasm ? 'web_wasm' : 'web');
919 920
}

921
/// Returns the Linux build output directory.
922
String getLinuxBuildDirectory([TargetPlatform? targetPlatform]) {
923 924 925
  final String arch = (targetPlatform == null) ?
      _getCurrentHostPlatformArchName() :
      getNameForTargetPlatformArch(targetPlatform);
926
  final String subDirs = 'linux/$arch';
927
  return globals.fs.path.join(getBuildDirectory(), subDirs);
928 929
}

930 931
/// Returns the Windows build output directory.
String getWindowsBuildDirectory() {
932
  return globals.fs.path.join(getBuildDirectory(), 'windows');
933 934
}

935 936
/// Returns the Fuchsia build output directory.
String getFuchsiaBuildDirectory() {
937
  return globals.fs.path.join(getBuildDirectory(), 'fuchsia');
938
}
939 940 941 942 943 944

/// Defines specified via the `--dart-define` command-line option.
///
/// These values are URI-encoded and then combined into a comma-separated string.
const String kDartDefines = 'DartDefines';

945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993
/// The define to pass a [BuildMode].
const String kBuildMode = 'BuildMode';

/// The define to pass whether we compile 64-bit android-arm code.
const String kTargetPlatform = 'TargetPlatform';

/// The define to control what target file is used.
const String kTargetFile = 'TargetFile';

/// Whether to enable or disable track widget creation.
const String kTrackWidgetCreation = 'TrackWidgetCreation';

/// Additional configuration passed to the dart front end.
///
/// This is expected to be a comma separated list of strings.
const String kExtraFrontEndOptions = 'ExtraFrontEndOptions';

/// Additional configuration passed to gen_snapshot.
///
/// This is expected to be a comma separated list of strings.
const String kExtraGenSnapshotOptions = 'ExtraGenSnapshotOptions';

/// Whether the build should run gen_snapshot as a split aot build for deferred
/// components.
const String kDeferredComponents = 'DeferredComponents';

/// Whether to strip source code information out of release builds and where to save it.
const String kSplitDebugInfo = 'SplitDebugInfo';

/// Alternative scheme for file URIs.
///
/// May be used along with [kFileSystemRoots] to support a multi-root
/// filesystem.
const String kFileSystemScheme = 'FileSystemScheme';

/// Additional filesystem roots.
///
/// If provided, must be used along with [kFileSystemScheme].
const String kFileSystemRoots = 'FileSystemRoots';

/// The define to control what iOS architectures are built for.
///
/// This is expected to be a space-delimited list of architectures. If not
/// provided, defaults to arm64.
const String kIosArchs = 'IosArchs';

/// The define to control what macOS architectures are built for.
///
/// This is expected to be a space-delimited list of architectures. If not
994
/// provided, defaults to x86_64.
995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
///
/// Supported values are x86_64 and arm64.
const String kDarwinArchs = 'DarwinArchs';

/// Path to the SDK root to be used as the isysroot.
const String kSdkRoot = 'SdkRoot';

/// Whether to enable Dart obfuscation and where to save the symbol map.
const String kDartObfuscation = 'DartObfuscation';

/// An output directory where one or more code-size measurements may be written.
const String kCodeSizeDirectory = 'CodeSizeDirectory';

/// SHA identifier of the Apple developer code signing identity.
///
/// Same as EXPANDED_CODE_SIGN_IDENTITY Xcode build setting.
/// Also discoverable via `security find-identity -p codesigning`.
const String kCodesignIdentity = 'CodesignIdentity';

/// The build define controlling whether icon fonts should be stripped down to
/// only the glyphs used by the application.
const String kIconTreeShakerFlag = 'TreeShakeIcons';

/// The input key for an SkSL bundle path.
const String kBundleSkSLPath = 'BundleSkSLPath';
1020 1021 1022 1023 1024 1025

/// The define to pass build name
const String kBuildName = 'BuildName';

/// The define to pass build number
const String kBuildNumber = 'BuildNumber';
1026

1027 1028 1029 1030 1031
/// The action Xcode is taking.
///
/// Will be "build" when building and "install" when archiving.
const String kXcodeAction = 'Action';

1032 1033 1034 1035 1036 1037 1038 1039 1040 1041
final Converter<String, String> _defineEncoder = utf8.encoder.fuse(base64.encoder);
final Converter<String, String> _defineDecoder = base64.decoder.fuse(utf8.decoder);

/// Encode a List of dart defines in a base64 string.
///
/// This encoding does not include `,`, which is used to distinguish
/// the individual entries, nor does it include `%` which is often a
/// control character on windows command lines.
///
/// When decoding this string, it can be safely split on commas, since any
1042
/// user provided commands will still be encoded.
1043 1044 1045
///
/// If the presence of the `/` character ends up being an issue, this can
/// be changed to use base32 instead.
1046
String encodeDartDefines(List<String> defines) {
1047 1048 1049 1050
  return defines.map(_defineEncoder.convert).join(',');
}

List<String> decodeCommaSeparated(Map<String, String> environmentDefines, String key) {
1051
  if (!environmentDefines.containsKey(key) || environmentDefines[key]!.isEmpty) {
1052 1053
    return <String>[];
  }
1054
  return environmentDefines[key]!
1055 1056 1057
    .split(',')
    .cast<String>()
    .toList();
1058 1059 1060
}

/// Dart defines are encoded inside [environmentDefines] as a comma-separated list.
1061
List<String> decodeDartDefines(Map<String, String> environmentDefines, String key) {
1062
  if (!environmentDefines.containsKey(key) || environmentDefines[key]!.isEmpty) {
1063
    return <String>[];
1064
  }
1065
  return environmentDefines[key]!
1066
    .split(',')
1067
    .map<Object>(_defineDecoder.convert)
1068 1069 1070
    .cast<String>()
    .toList();
}
1071 1072 1073 1074 1075

/// The null safety runtime mode the app should be built in.
enum NullSafetyMode {
  sound,
  unsound,
1076
  /// The null safety mode was not detected. Only supported for 'flutter test'.
1077 1078
  autodetect,
}
1079 1080 1081 1082 1083 1084 1085 1086 1087

String _getCurrentHostPlatformArchName() {
  final HostPlatform hostPlatform = getCurrentHostPlatform();
  return getNameForHostPlatformArch(hostPlatform);
}

String getNameForTargetPlatformArch(TargetPlatform platform) {
  switch (platform) {
    case TargetPlatform.linux_x64:
1088
    case TargetPlatform.darwin:
1089 1090 1091 1092
    case TargetPlatform.windows_x64:
      return 'x64';
    case TargetPlatform.linux_arm64:
      return 'arm64';
1093 1094 1095 1096 1097 1098 1099 1100 1101 1102
    case TargetPlatform.android:
    case TargetPlatform.android_arm:
    case TargetPlatform.android_arm64:
    case TargetPlatform.android_x64:
    case TargetPlatform.android_x86:
    case TargetPlatform.fuchsia_arm64:
    case TargetPlatform.fuchsia_x64:
    case TargetPlatform.ios:
    case TargetPlatform.tester:
    case TargetPlatform.web_javascript:
1103
      throw UnsupportedError('Unexpected target platform $platform');
1104 1105 1106 1107 1108 1109 1110
  }
}

String getNameForHostPlatformArch(HostPlatform platform) {
  switch (platform) {
    case HostPlatform.darwin_x64:
      return 'x64';
1111 1112
    case HostPlatform.darwin_arm64:
      return 'arm64';
1113 1114 1115 1116 1117 1118 1119 1120
    case HostPlatform.linux_x64:
      return 'x64';
    case HostPlatform.linux_arm64:
      return 'arm64';
    case HostPlatform.windows_x64:
      return 'x64';
  }
}
1121 1122 1123 1124 1125 1126 1127

String? _uncapitalize(String? s) {
  if (s == null || s.isEmpty) {
    return s;
  }
  return s.substring(0, 1).toLowerCase() + s.substring(1);
}