From 864833fca9334ba0b986243fce790e89ff64d932 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Mon, 14 Apr 2025 07:33:27 -0700 Subject: [PATCH] Generate keep.xml to prevent resource shrinking on Android (#50620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: On Android, when resource shrinking is enabled, all resources added by Metro may be removed as react-native is accessing resources based on strings rather than references, so AGP can't see its usage. Example output of `android/app/build/outputs/mapping/release/resources.txt `when `shrinkResources` is enabled. ``` raw/__node_modules_expo_vectoricons_build_vendor_reactnativevectoricons_fonts_materialcommunityicons : reachable=false drawable/__common_assets_haptics_icon : reachable=false ``` It’s a coincidence that most of the resources are currently working, as many file names begin with strings that already exist in the String Pool. For example, `node_modules...` is flagged as used because 'node' is present in the String Pool, causing it to be whitelisted. However, this does not guarantee that the same will apply to all files - especially in a monorepo setup, where paths are significantly different. For example * `__node_modules_expo_vectoricons_build_vendor_reactnativevectoricons_fonts_materialcommunityicons` * `__common_assets_haptics_icon` To prevent that behavior, metro during assets export should create `keep.xml` listing all resources generated by metro. https://developer.android.com/build/shrink-code#keep-resources We have already made a similar change in expo cli: https://github.com/expo/expo/pull/35465 ## Changelog: [ANDROID][ADDED] - Generate keep.xml to prevent resource shrinking Pull Request resolved: https://github.com/facebook/react-native/pull/50620 Test Plan: 1. Enable resource shrinking in RNTester by adding this to `android.buildTypes.release` to `packages/rn-tester/android/app/build.gradle.kts` ```gradle isMinifyEnabled = true isShrinkResources = true ``` 2. Use some resources in playground, for example: ```diff diff --git a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js index 9dbacb99701..9ac9c231f3f 100644 --- a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js +++ b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js @@ -11,16 +11,14 @@ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; -import RNTesterText from '../../components/RNTesterText'; import * as React from 'react'; import {StyleSheet, View} from 'react-native'; +import {Header} from "react-native/Libraries/NewAppScreen"; function Playground() { return ( - - Edit "RNTesterPlayground.js" to change this file - +
); } ``` 3. Build app using `hermesRelease` variant 4. See Playground screen | Before | After | |---|---| | Zrzut ekranu 2025-04-10 o 12 17 53 | Zrzut ekranu 2025-04-10 o 12 15 58 | 5. Inspect `packages/rn-tester/android/app/build/outputs/mapping/hermesRelease/resources.txt` | Before | After | |---|---| | `drawable/_reactnative_libraries_newappscreen_components_logo : reachable=false` | `drawable/_reactnative_libraries_newappscreen_components_logo : reachable=true` | Reviewed By: cortinico Differential Revision: D72960028 Pulled By: huntie fbshipit-source-id: df725fa2ea50150cd67687a97986976ffbbb5b40 --- .../src/commands/bundle/assetPathUtils.js | 3 +- .../commands/bundle/createKeepFileAsync.js | 40 +++++++++++++++++++ .../src/commands/bundle/saveAssets.js | 4 ++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js diff --git a/packages/community-cli-plugin/src/commands/bundle/assetPathUtils.js b/packages/community-cli-plugin/src/commands/bundle/assetPathUtils.js index 0207bb7951a..84261469a32 100644 --- a/packages/community-cli-plugin/src/commands/bundle/assetPathUtils.js +++ b/packages/community-cli-plugin/src/commands/bundle/assetPathUtils.js @@ -40,7 +40,7 @@ function getAndroidAssetSuffix(scale: number): string { } // See https://developer.android.com/guide/topics/resources/drawable-resource.html -const drawableFileTypes = new Set([ +const drawableFileTypes: Set = new Set([ 'gif', 'jpeg', 'jpg', @@ -86,6 +86,7 @@ function getBasePath(asset: PackagerAsset): string { } export default { + drawableFileTypes, getAndroidAssetSuffix, getAndroidResourceFolderName, getResourceIdentifier, diff --git a/packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js b/packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js new file mode 100644 index 00000000000..dd347b6b319 --- /dev/null +++ b/packages/community-cli-plugin/src/commands/bundle/createKeepFileAsync.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import type {AssetData} from 'metro/src/Assets'; + +import assetPathUtils from './assetPathUtils'; +import fs from 'fs'; +import path from 'path'; + +async function createKeepFileAsync( + assets: $ReadOnlyArray, + outputDirectory: string, +): Promise { + if (!assets.length) { + return; + } + const assetsList = []; + for (const asset of assets) { + const prefix = assetPathUtils.drawableFileTypes.has(asset.type) + ? 'drawable' + : 'raw'; + assetsList.push( + `@${prefix}/${assetPathUtils.getResourceIdentifier(asset)}`, + ); + } + const keepPath = path.join(outputDirectory, 'raw/keep.xml'); + const content = `\n`; + await fs.promises.mkdir(path.dirname(keepPath), {recursive: true}); + await fs.promises.writeFile(keepPath, content); +} + +export default createKeepFileAsync; diff --git a/packages/community-cli-plugin/src/commands/bundle/saveAssets.js b/packages/community-cli-plugin/src/commands/bundle/saveAssets.js index cb40997c322..9f168d7777c 100644 --- a/packages/community-cli-plugin/src/commands/bundle/saveAssets.js +++ b/packages/community-cli-plugin/src/commands/bundle/saveAssets.js @@ -17,6 +17,7 @@ import { isCatalogAsset, writeImageSet, } from './assetCatalogIOS'; +import createKeepFileAsync from './createKeepFileAsync'; import filterPlatformAssetScales from './filterPlatformAssetScales'; import getAssetDestPathAndroid from './getAssetDestPathAndroid'; import getAssetDestPathIOS from './getAssetDestPathIOS'; @@ -88,6 +89,9 @@ async function saveAssets( } else { assets.forEach(addAssetToCopy); } + if (platform === 'android') { + await createKeepFileAsync(assets, assetsDest); + } return copyAll(filesToCopy); }