flutter/packages/flutter_tools/test/general.shard/base/build_test.dart
Chris Bracken 09d4dabd6d
iOS: Update minimum iOS version to 13.0 (#167737)
This updates the Flutter minimum iOS version from 12.0 to 13.0, adds a
migrator for existing apps, and updates our own examples, tests, and
benchmark apps to 13.0. A follow-up patch will drop iOS 13 `@available`
checks in the embedder.

This is required in order to use Swift in the embedder and not need to
bundle the Swift runtime libs in every app that uses Flutter. Swift
stable ABI

As of March 2025, usage of iOS is well below 1%, see example public
usage data here:
https://telemetrydeck.com/survey/apple/iOS/majorSystemVersions/

This patch makes the following changes:
1. Updates ios_deployment_target from 12.0 to 13.0.
2. Changes templates to `IPHONEOS_DEPLOYMENT_TARGET`,
`MinimumOSVersion`, and Podfile `platform :ios` to 12.0.
3. Adds migrator for Podfile part to migrate `platform :ios, '11.0'` ->
`platform :ios, '12.0'`
4. Compiles with `-miphoneos-version-min=12.0`
5. Runs the migrator on all example apps and integration tests.
6. Updates examples, tests to iOS 13 deployment target

It also updates `verify_exported.dart`:
* iOS 13 introduces stricter separation of const and non-const global
symbols. Previously, these were declared in the Mach-O `__DATA` section
which may be mapped read-write, but now they're in a dedicated
`__DATA_CONST` section which is mapped read-only. This adds
`(__DATA_CONST,__const)` to the allowlist with the same enforcement on
exported symbol naming as before.

See also (ios_deployment_target):
* https://github.com/flutter/buildroot/pull/808
* https://github.com/flutter/buildroot/pull/574

See also (template, migrator):
* https://github.com/flutter/flutter/pull/62902
* https://github.com/flutter/flutter/pull/85174
* https://github.com/flutter/flutter/pull/101963
* https://github.com/flutter/flutter/pull/140478

Issue: https://github.com/flutter/flutter/issues/167735

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-04-24 20:15:13 +00:00

618 lines
19 KiB
Dart

// 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/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/build.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
const FakeCommand kWhichSysctlCommand = FakeCommand(command: <String>['which', 'sysctl']);
const FakeCommand kARMCheckCommand = FakeCommand(
command: <String>['sysctl', 'hw.optional.arm64'],
exitCode: 1,
);
const List<String> kDefaultClang = <String>[
'-miphoneos-version-min=13.0',
'-isysroot',
'path/to/sdk',
'-dynamiclib',
'-Xlinker',
'-rpath',
'-Xlinker',
'@executable_path/Frameworks',
'-Xlinker',
'-rpath',
'-Xlinker',
'@loader_path/Frameworks',
'-fapplication-extension',
'-install_name',
'@rpath/App.framework/App',
'-o',
'build/foo/App.framework/App',
'build/foo/snapshot_assembly.o',
];
void main() {
group('GenSnapshot', () {
late GenSnapshot genSnapshot;
late Artifacts artifacts;
late FakeProcessManager processManager;
late BufferLogger logger;
setUp(() async {
artifacts = Artifacts.test();
logger = BufferLogger.test();
processManager = FakeProcessManager.list(<FakeCommand>[]);
genSnapshot = GenSnapshot(
artifacts: artifacts,
logger: logger,
processManager: processManager,
);
});
testWithoutContext('android_x64', () async {
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_x64,
mode: BuildMode.release,
),
'--additional_arg',
],
),
);
final int result = await genSnapshot.run(
snapshotType: SnapshotType(TargetPlatform.android_x64, BuildMode.release),
additionalArgs: <String>['--additional_arg'],
);
expect(result, 0);
});
testWithoutContext('iOS arm64', () async {
final String genSnapshotPath = artifacts.getArtifactPath(
Artifact.genSnapshotArm64,
platform: TargetPlatform.ios,
mode: BuildMode.release,
);
processManager.addCommand(
FakeCommand(command: <String>[genSnapshotPath, '--additional_arg']),
);
final int result = await genSnapshot.run(
snapshotType: SnapshotType(TargetPlatform.ios, BuildMode.release),
darwinArch: DarwinArch.arm64,
additionalArgs: <String>['--additional_arg'],
);
expect(result, 0);
});
testWithoutContext('--strip filters error output from gen_snapshot', () async {
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_x64,
mode: BuildMode.release,
),
'--strip',
],
stderr: 'ABC\n${GenSnapshot.kIgnoredWarnings.join('\n')}\nXYZ\n',
),
);
final int result = await genSnapshot.run(
snapshotType: SnapshotType(TargetPlatform.android_x64, BuildMode.release),
additionalArgs: <String>['--strip'],
);
expect(result, 0);
expect(logger.errorText, contains('ABC'));
for (final String ignoredWarning in GenSnapshot.kIgnoredWarnings) {
expect(logger.errorText, isNot(contains(ignoredWarning)));
}
expect(logger.errorText, contains('XYZ'));
});
});
group('AOTSnapshotter', () {
late MemoryFileSystem fileSystem;
late AOTSnapshotter snapshotter;
late Artifacts artifacts;
late FakeProcessManager processManager;
setUp(() async {
fileSystem = MemoryFileSystem.test();
artifacts = Artifacts.test();
processManager = FakeProcessManager.empty();
snapshotter = AOTSnapshotter(
fileSystem: fileSystem,
logger: BufferLogger.test(),
xcode: Xcode.test(processManager: processManager),
artifacts: artifacts,
processManager: processManager,
);
});
testWithoutContext('does not build iOS with debug build mode', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
expect(
await snapshotter.build(
platform: TargetPlatform.ios,
darwinArch: DarwinArch.arm64,
sdkRoot: 'path/to/sdk',
buildMode: BuildMode.debug,
mainPath: 'main.dill',
outputPath: outputPath,
dartObfuscation: false,
),
isNot(equals(0)),
);
});
testWithoutContext('does not build android-arm with debug build mode', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
expect(
await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.debug,
mainPath: 'main.dill',
outputPath: outputPath,
dartObfuscation: false,
),
isNot(0),
);
});
testWithoutContext('does not build android-arm64 with debug build mode', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
expect(
await snapshotter.build(
platform: TargetPlatform.android_arm64,
buildMode: BuildMode.debug,
mainPath: 'main.dill',
outputPath: outputPath,
dartObfuscation: false,
),
isNot(0),
);
});
testWithoutContext('builds iOS snapshot with dwarfStackTraces', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
final String assembly = fileSystem.path.join(outputPath, 'snapshot_assembly.S');
final String debugPath = fileSystem.path.join('foo', 'app.ios-arm64.symbols');
final String genSnapshotPath = artifacts.getArtifactPath(
Artifact.genSnapshotArm64,
platform: TargetPlatform.ios,
mode: BuildMode.profile,
);
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
genSnapshotPath,
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=$assembly',
'--dwarf-stack-traces',
'--resolve-dwarf-paths',
'--save-debugging-info=$debugPath',
'main.dill',
],
),
kWhichSysctlCommand,
kARMCheckCommand,
const FakeCommand(
command: <String>[
'xcrun',
'cc',
'-arch',
'arm64',
'-miphoneos-version-min=13.0',
'-isysroot',
'path/to/sdk',
'-c',
'build/foo/snapshot_assembly.S',
'-o',
'build/foo/snapshot_assembly.o',
],
),
const FakeCommand(command: <String>['xcrun', 'clang', '-arch', 'arm64', ...kDefaultClang]),
const FakeCommand(
command: <String>[
'xcrun',
'dsymutil',
'-o',
'build/foo/App.framework.dSYM',
'build/foo/App.framework/App',
],
),
const FakeCommand(
command: <String>[
'xcrun',
'strip',
'-x',
'build/foo/App.framework/App',
'-o',
'build/foo/App.framework/App',
],
),
]);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.ios,
buildMode: BuildMode.profile,
mainPath: 'main.dill',
outputPath: outputPath,
darwinArch: DarwinArch.arm64,
sdkRoot: 'path/to/sdk',
splitDebugInfo: 'foo',
dartObfuscation: false,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('builds iOS snapshot with obfuscate', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
final String assembly = fileSystem.path.join(outputPath, 'snapshot_assembly.S');
final String genSnapshotPath = artifacts.getArtifactPath(
Artifact.genSnapshotArm64,
platform: TargetPlatform.ios,
mode: BuildMode.profile,
);
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
genSnapshotPath,
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=$assembly',
'--obfuscate',
'main.dill',
],
),
kWhichSysctlCommand,
kARMCheckCommand,
const FakeCommand(
command: <String>[
'xcrun',
'cc',
'-arch',
'arm64',
'-miphoneos-version-min=13.0',
'-isysroot',
'path/to/sdk',
'-c',
'build/foo/snapshot_assembly.S',
'-o',
'build/foo/snapshot_assembly.o',
],
),
const FakeCommand(command: <String>['xcrun', 'clang', '-arch', 'arm64', ...kDefaultClang]),
const FakeCommand(
command: <String>[
'xcrun',
'dsymutil',
'-o',
'build/foo/App.framework.dSYM',
'build/foo/App.framework/App',
],
),
const FakeCommand(
command: <String>[
'xcrun',
'strip',
'-x',
'build/foo/App.framework/App',
'-o',
'build/foo/App.framework/App',
],
),
]);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.ios,
buildMode: BuildMode.profile,
mainPath: 'main.dill',
outputPath: outputPath,
darwinArch: DarwinArch.arm64,
sdkRoot: 'path/to/sdk',
dartObfuscation: true,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('builds iOS snapshot', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
final String genSnapshotPath = artifacts.getArtifactPath(
Artifact.genSnapshotArm64,
platform: TargetPlatform.ios,
mode: BuildMode.release,
);
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
genSnapshotPath,
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=${fileSystem.path.join(outputPath, 'snapshot_assembly.S')}',
'main.dill',
],
),
kWhichSysctlCommand,
kARMCheckCommand,
const FakeCommand(
command: <String>[
'xcrun',
'cc',
'-arch',
'arm64',
'-miphoneos-version-min=13.0',
'-isysroot',
'path/to/sdk',
'-c',
'build/foo/snapshot_assembly.S',
'-o',
'build/foo/snapshot_assembly.o',
],
),
const FakeCommand(command: <String>['xcrun', 'clang', '-arch', 'arm64', ...kDefaultClang]),
const FakeCommand(
command: <String>[
'xcrun',
'dsymutil',
'-o',
'build/foo/App.framework.dSYM',
'build/foo/App.framework/App',
],
),
const FakeCommand(
command: <String>[
'xcrun',
'strip',
'-x',
'build/foo/App.framework/App',
'-o',
'build/foo/App.framework/App',
],
),
]);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.ios,
buildMode: BuildMode.release,
mainPath: 'main.dill',
outputPath: outputPath,
darwinArch: DarwinArch.arm64,
sdkRoot: 'path/to/sdk',
dartObfuscation: false,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('builds shared library for android-arm (32bit)', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_arm,
mode: BuildMode.release,
),
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'--strip',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'main.dill',
],
),
);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
outputPath: outputPath,
dartObfuscation: false,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('builds shared library for android-arm with dwarf stack traces', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
final String debugPath = fileSystem.path.join('foo', 'app.android-arm.symbols');
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_arm,
mode: BuildMode.release,
),
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'--strip',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'--dwarf-stack-traces',
'--resolve-dwarf-paths',
'--save-debugging-info=$debugPath',
'main.dill',
],
),
);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
outputPath: outputPath,
splitDebugInfo: 'foo',
dartObfuscation: false,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('builds shared library for android-arm with obfuscate', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_arm,
mode: BuildMode.release,
),
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'--strip',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'--obfuscate',
'main.dill',
],
),
);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
outputPath: outputPath,
dartObfuscation: true,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext(
'builds shared library for android-arm without dwarf stack traces due to empty string',
() async {
final String outputPath = fileSystem.path.join('build', 'foo');
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_arm,
mode: BuildMode.release,
),
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'--strip',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'main.dill',
],
),
);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
outputPath: outputPath,
splitDebugInfo: '',
dartObfuscation: false,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
},
);
testWithoutContext('builds shared library for android-arm64', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_arm64,
mode: BuildMode.release,
),
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'--strip',
'main.dill',
],
),
);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm64,
buildMode: BuildMode.release,
mainPath: 'main.dill',
outputPath: outputPath,
dartObfuscation: false,
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('--no-strip in extraGenSnapshotOptions suppresses --strip', () async {
final String outputPath = fileSystem.path.join('build', 'foo');
processManager.addCommand(
FakeCommand(
command: <String>[
artifacts.getArtifactPath(
Artifact.genSnapshot,
platform: TargetPlatform.android_arm64,
mode: BuildMode.release,
),
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'main.dill',
],
),
);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm64,
buildMode: BuildMode.release,
mainPath: 'main.dill',
outputPath: outputPath,
dartObfuscation: false,
extraGenSnapshotOptions: const <String>['--no-strip'],
);
expect(genSnapshotExitCode, 0);
expect(processManager, hasNoRemainingExpectations);
});
});
}