Skip to content

[native assets] Respect Apple deployment targets - #194146

Open
leehack wants to merge 9 commits into
flutter:masterfrom
leehack:fix/native-assets-ios-deployment-target
Open

leehack wants to merge 9 commits into
flutter:masterfrom
leehack:fix/native-assets-ios-deployment-target

Conversation

@leehack

@leehack leehack commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Addresses #194145.

Pass Apple deployment versions from Xcode through flutter assemble into build/link hook configuration using AppleVersion, and use the same resolved iOS value for native framework MinimumOSVersion. Preserve native-assets defaults/floors of iOS 15 and macOS 13, with actionable errors for malformed or overflowing components. macOS framework layout is unchanged.

The precision API merged in dart-lang/native#3742. It shipped in code_assets 2.2.0, which Flutter master pins since #196114; this branch is merged up to master at 031636eb69.

macOS app target

macos_assemble.sh runs in the Flutter Assemble aggregate target, which cannot read Runner's MACOSX_DEPLOYMENT_TARGET. buildMacOS already resolves Runner's build settings, so it now writes that value to Flutter-Generated.xcconfig as FLUTTER_MACOS_DEPLOYMENT_TARGET, and xcode_backend.dart prefers it over the aggregate target's own setting.

Known limitation: the value is refreshed only by the Flutter tool. A build started directly from Xcode uses the value from the last flutter build/flutter run, or the aggregate target's setting if none was written.

Validation

  • After the master merge: the 125 tests in the affected test files pass, and analysis (--fatal-infos) and formatting of the changed files pass, using Dart 3.15.0-132.0.dev.
  • macOS debug build with Xcode 27.2, package_ffi dependency, Runner at 14.4.1 and project/aggregate at 12.0: the hook input has deployment version 14.4.1 and the built dylib has minos 14.4.1.
  • Before the master merge, with Xcode 27.2: reduced C-hook app, with no platform folders in its dependency: unsigned iOS release at 16.4; no-clean rebuild at 17.2.1; debug simulator at 17.2.1 for arm64/x86_64. Hook inputs, binary minos, app metadata, and native framework metadata match.
  • Full Flutter CI, device execution, archive/App Store validation, and companion-free llamadart qualification remain unverified.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the AI contribution guidelines and understand my responsibilities, or I am not using AI tools.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant in-code documentation (doc comments with ///).
  • If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in flutter/website (or verified none is needed).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • I followed the breaking change policy and added Data Driven Fixes where supported.
  • All existing and new tests are passing. (Targeted tests pass; full Flutter CI is pending.)

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

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions Bot added tool Affects the "flutter" command-line tool. See also t: labels. team-ios Owned by iOS platform team team-macos Owned by the macOS platform team labels Oct 10, 2026
@leehack
leehack marked this pull request as ready for review October 11, 2026 12:48
@leehack
leehack requested review from a team as code owners October 11, 2026 12:48

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the iOS deployment target (IPHONEOS_DEPLOYMENT_TARGET) when packaging native code assets, passing it from Xcode and validating it against the minimum supported iOS version. Feedback on the changes highlights two compilation issues: an invalid syntax pattern (?deploymentTarget) in the test file and a type mismatch where a double is passed to the Version constructor instead of an int.

@dcharkes

Copy link
Copy Markdown
Contributor

I think we should aim for the version number that comes from the xcode build to flow into flutter assemble which should then flow properly into the build hooks (and the metadata). Only fixing the metadata makes it inconsistent.

(Maybe we should do MacOS and iOS in the same PR? Aren't their constraints rather similar?)

Here is an AI explanation on how to do this following a similar pattern as #181004 takes for wiring the ccompiler info from cmake to the build hooks:

To wire the deployment target into IOSCodeConfig (and optionally MacOSCodeConfig) following the same pattern as Android's targetAndroidNdkApi(environmentDefines), the version resolution should live in ios/native_assets.dart as a function of environmentDefines and be consumed by both IOSAssetTarget (during BuildHooks / LinkHooks) and copyNativeCodeAssetsIOS (during InstallCodeAssets).

Here is the step-by-step wiring across the 4 files:

1. Forward the Xcode build setting in xcode_backend.dart & build_info.dart

(PR #194146 already does this part for iOS)

  • In build_info.dart:
    const kIosDeploymentTarget = 'IosDeploymentTarget';
  • In xcode_backend.dart:
    final String? deploymentTarget = environment['IPHONEOS_DEPLOYMENT_TARGET'];
    if (deploymentTarget != null) {
      flutterArgs.add('-d$kIosDeploymentTarget=$deploymentTarget');
    }

2. Replace const targetIOSVersion = 15 with a Version-returning helper in ios/native_assets.dart

Mirror targetAndroidNdkApi(Map<String, String> environmentDefines) so both targets.dart and copyNativeCodeAssetsIOS use the same function (and use FlutterDarwinPlatform.ios.deploymentTarget() as the baseline):

Version targetIOSVersion(Map<String, String> environmentDefines) {
  final Version minimumSupported = FlutterDarwinPlatform.ios.deploymentTarget();
  final String? target = environmentDefines[kIosDeploymentTarget]?.trim();
  if (target == null || target.isEmpty) {
    return minimumSupported;
  }
  final Version? version = Version.parse(target);
  if (version == null || Version.versionPattern.firstMatch(target)?.end != target.length) {
    throwToolExit(
      'Invalid iOS deployment target "${environmentDefines[kIosDeploymentTarget]}". '
      'Expected a version such as 16, 16.4, or 17.2.1.',
    );
  }
  if (version < minimumSupported) {
    return minimumSupported;
  }
  // Ensure major-only inputs like "16" format as "16.0" in Info.plist while
  // preserving minor/patch versions like "16.4" or "17.2.1".
  if (!target.contains('.')) {
    return Version(version.major, version.minor, null);
  }
  return version;
}

3. Wire targetIOSVersion(environmentDefines) into IOSAssetTarget in targets.dart

IOSAssetTarget already receives environmentDefines, so _getIOSConfig in targets.dart only needs to call targetIOSVersion(environmentDefines).major:

  IOSCodeConfig _getIOSConfig(Map<String, String> environmentDefines, FileSystem fileSystem) {
    final String? sdkRoot = environmentDefines[kSdkRoot];
    if (sdkRoot == null) {
      throw MissingDefineException(kSdkRoot, 'native_assets');
    }
    final EnvironmentType environmentType = xcode.environmentTypeFromSdkroot(sdkRoot, fileSystem);
    return IOSCodeConfig(
      targetVersion: targetIOSVersion(environmentDefines).major,
      targetSdk: getIOSSdk(environmentType),
    );
  }

Because AssetBuildTarget.targetsFor(...) is called at the start of BuildHooks (and LinkHooks):

  • Build and link hooks receive input.config.code.iOS.targetVersion == 16 (instead of 15) when IPHONEOS_DEPLOYMENT_TARGET is 16.4.
  • Any malformed kIosDeploymentTarget fails fast during build_hooks before running any hooks or copying frameworks.
  • Once package:code_assets supports minor/patch versions in IOSCodeConfig, targetIOSVersion(environmentDefines) already returns a full Version object with .major, .minor, and .patch.

4. Use the same helper in installCodeAssets / copyNativeCodeAssetsIOS

Instead of passing raw String? iosDeploymentTarget and re-parsing it inside copyNativeCodeAssetsIOS, either:

  • Pass targetIOSVersion(environmentDefines) (or environmentDefines) to copyNativeCodeAssetsIOS, and pass minimumIOSVersion: deploymentTarget.toString() to createInfoPlist.

(And if doing macOS at the same time: MacOSAssetTarget can take Map<String, String> environmentDefines in _macTargets—and const <String, String>{} in FlutterTesterAssetTarget—and call targetMacOSVersion(environmentDefines).major.)

@leehack

leehack commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

@dcharkes Agreed that the app deployment target should reach both build/link hooks and the packaged metadata, and that iOS and macOS should be handled together.

The remaining precision prerequisite is in package:code_assets: both Apple targetVersion fields are integers, so passing .major still truncates values such as 16.4 or 17.2.1. I opened dart-lang/native#3741 and draft dart-lang/native#3742 proposing compatible optional minor/patch fields and migrating the C toolchain, Objective-C hook, and ffigen test hook. The draft covers iOS and macOS; it has 692 passing affected tests, but full CI is not green and the baseline failures are disclosed there.

Once the API shape is agreed and available for Flutter to consume, the follow-up here is to use one resolved version per platform for build/link configuration and metadata, forward MACOSX_DEPLOYMENT_TARGET as well as IPHONEOS_DEPLOYMENT_TARGET, and add propagation/cache/default/error tests for both platforms. I will preserve the existing native-assets floors (iOS 15 and macOS 13); the shared Darwin macOS floor is currently 12, so switching to it directly would change behavior.

This Flutter patch currently only fixes iOS framework metadata. It does not yet implement full hook propagation or the macOS configuration change.

@github-actions github-actions Bot added the a: desktop Running on desktop label Oct 31, 2026
@leehack leehack changed the title [native assets] Respect the iOS deployment target in framework metadata [native assets] Respect Apple deployment targets Oct 31, 2026
@github-actions github-actions Bot added platform-ios iOS applications specifically platform-macos Building on or for macOS specifically labels Nov 10, 2026
@leehack
leehack marked this pull request as ready for review November 10, 2026 03:04
@leehack

leehack commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

@dcharkes this should be ready for another look. I went with your suggestion: the deployment target now goes through flutter assemble into the build and link hooks as well as the framework metadata, and iOS and macOS are both in this PR. Minor and patch versions are kept now that code_assets 2.2.0 is rolled in.

macOS needed one extra step. The assemble script runs in the Flutter Assemble target, which doesn't see Runner's MACOSX_DEPLOYMENT_TARGET, so I have buildMacOS write Runner's value into Flutter-Generated.xcconfig and the script picks it up from there. The downside is that a build started straight from Xcode gets whatever the last flutter build or flutter run wrote. Let me know if you'd rather solve that differently.

Could someone add the CICD label so the full suite runs? Thanks!

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request forwards iOS and macOS deployment targets from Xcode build settings to the Flutter tool, ensuring that native asset hooks and framework metadata (such as the Info.plist MinimumOSVersion) respect the configured deployment targets. Feedback is provided to improve the readability of the regex match validation in apple_deployment_version.dart by explicitly checking the match properties.

Comment on lines +29 to +31
if (parsed == null || Version.versionPattern.firstMatch(target)?.end != target.length) {
invalidTarget();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current regex match check Version.versionPattern.firstMatch(target)?.end != target.length relies on implicit null-coalescing behavior which can be difficult to read and reason about.\n\nWe can improve readability and clarity by explicitly checking if the match is non-null, starts at index 0, and spans the entire length of the target string. This also aligns with the Flutter style guide principle of optimizing for readability.

  final RegExpMatch? match = Version.versionPattern.firstMatch(target);\n  if (parsed == null || match == null || match.start != 0 || match.end != target.length) {\n    invalidTarget();\n  }
References
  1. Optimize for readability: Code is read more often than it is written. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern is anchored with ^ and parsed == null covers the no-match case, so the only thing left to check is that the match reaches the end.

@dcharkes dcharkes added the CICD Run CI/CD label Nov 10, 2026
@dcharkes

Copy link
Copy Markdown
Contributor

Thanks @leehack! 🙏

macOS needed one extra step. The assemble script runs in the Flutter Assemble target, which doesn't see Runner's MACOSX_DEPLOYMENT_TARGET, so I have buildMacOS write Runner's value into Flutter-Generated.xcconfig and the script picks it up from there. The downside is that a build started straight from Xcode gets whatever the last flutter build or flutter run wrote. Let me know if you'd rather solve that differently.

That is somewhat unfortunate. However, it's more important for iOS where we have add2app and there might not be an outer flutter run to populate the value. So I think this is okay.

FYI @bkonyi

@LouiseHsu
LouiseHsu requested a review from vashworth November 12, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop CICD Run CI/CD platform-ios iOS applications specifically platform-macos Building on or for macOS specifically team-ios Owned by iOS platform team team-macos Owned by the macOS platform team tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants