@expo/repack-app 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/apktool_2.10.0.jar +0 -0
- package/build/android/apktool.d.ts +21 -0
- package/build/android/apktool.js +165 -0
- package/build/android/build-tools.d.ts +7 -14
- package/build/android/build-tools.js +26 -60
- package/build/android/index.js +45 -14
- package/build/android/resources.d.ts +54 -3
- package/build/android/resources.js +109 -114
- package/build/cli.js +18 -4
- package/build/expo.d.ts +11 -0
- package/build/expo.js +74 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +15 -0
- package/build/ios/build-tools.d.ts +5 -1
- package/build/ios/build-tools.js +37 -2
- package/build/ios/index.d.ts +1 -1
- package/build/ios/index.js +33 -15
- package/build/ios/resources.d.ts +19 -2
- package/build/ios/resources.js +50 -5
- package/build/types.d.ts +19 -2
- package/package.json +16 -4
- package/assets/Configuration.proto +0 -216
- package/assets/Resources.proto +0 -648
- package/build/android/Resources.types.d.ts +0 -93
- package/build/android/Resources.types.js +0 -5
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NormalizedOptions } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Decode the APK file using APKTool.
|
|
4
|
+
*/
|
|
5
|
+
export declare function decodeApkAsync(apkPath: string, options: NormalizedOptions): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Rebuild the decoded APK file using APKTool.
|
|
8
|
+
*/
|
|
9
|
+
export declare function rebuildApkAsync(decodedApkRoot: string, options: NormalizedOptions): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Add the bundle assets to the decoded APK.
|
|
12
|
+
*/
|
|
13
|
+
export declare function addBundleAssetsToDecodedApkAsync({ decodedApkRoot, assetRoot, bundleOutputPath, }: {
|
|
14
|
+
decodedApkRoot: string;
|
|
15
|
+
assetRoot: string;
|
|
16
|
+
bundleOutputPath: string;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Update the apktool.yml `renameManifestPackage`
|
|
20
|
+
*/
|
|
21
|
+
export declare function addRenameManifestPackageAsync(decodedApkRoot: string, packageName: string): Promise<void>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.addRenameManifestPackageAsync = exports.addBundleAssetsToDecodedApkAsync = exports.rebuildApkAsync = exports.decodeApkAsync = void 0;
|
|
7
|
+
const steps_1 = require("@expo/steps");
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const glob_1 = require("glob");
|
|
10
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const xml2js_1 = require("xml2js");
|
|
13
|
+
let cachedApktoolPath = null;
|
|
14
|
+
/**
|
|
15
|
+
* Decode the APK file using APKTool.
|
|
16
|
+
*/
|
|
17
|
+
async function decodeApkAsync(apkPath, options) {
|
|
18
|
+
const { workingDirectory } = options;
|
|
19
|
+
const apktoolPath = await getApktoolPathAsync();
|
|
20
|
+
const outputPath = 'decoded-apk';
|
|
21
|
+
await (0, steps_1.spawnAsync)('java', ['-jar', apktoolPath, 'decode', apkPath, '-s', '-o', outputPath], options.verbose
|
|
22
|
+
? { logger: options.logger, stdio: 'pipe', cwd: workingDirectory }
|
|
23
|
+
: { cwd: workingDirectory });
|
|
24
|
+
return node_path_1.default.join(workingDirectory, outputPath);
|
|
25
|
+
}
|
|
26
|
+
exports.decodeApkAsync = decodeApkAsync;
|
|
27
|
+
/**
|
|
28
|
+
* Rebuild the decoded APK file using APKTool.
|
|
29
|
+
*/
|
|
30
|
+
async function rebuildApkAsync(decodedApkRoot, options) {
|
|
31
|
+
const { workingDirectory } = options;
|
|
32
|
+
const apktoolPath = await getApktoolPathAsync();
|
|
33
|
+
const apktoolPackedApkName = 'apktool-packed.apk';
|
|
34
|
+
const apktoolPackedApkPath = node_path_1.default.resolve(workingDirectory, apktoolPackedApkName);
|
|
35
|
+
await (0, steps_1.spawnAsync)('java', ['-jar', apktoolPath, 'build', '-o', apktoolPackedApkPath, '--use-aapt2', decodedApkRoot], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
36
|
+
return apktoolPackedApkPath;
|
|
37
|
+
}
|
|
38
|
+
exports.rebuildApkAsync = rebuildApkAsync;
|
|
39
|
+
/**
|
|
40
|
+
* Add the bundle assets to the decoded APK.
|
|
41
|
+
*/
|
|
42
|
+
async function addBundleAssetsToDecodedApkAsync({ decodedApkRoot, assetRoot, bundleOutputPath, }) {
|
|
43
|
+
await promises_1.default.copyFile(bundleOutputPath, node_path_1.default.join(decodedApkRoot, 'assets', 'index.android.bundle'));
|
|
44
|
+
const assetSet = await copyAssetsAsync(assetRoot, node_path_1.default.join(decodedApkRoot, 'res'));
|
|
45
|
+
await addApktoolResourceAsync(decodedApkRoot, assetSet);
|
|
46
|
+
}
|
|
47
|
+
exports.addBundleAssetsToDecodedApkAsync = addBundleAssetsToDecodedApkAsync;
|
|
48
|
+
/**
|
|
49
|
+
* Update the apktool.yml `renameManifestPackage`
|
|
50
|
+
*/
|
|
51
|
+
async function addRenameManifestPackageAsync(decodedApkRoot, packageName) {
|
|
52
|
+
const apktoolYmlPath = node_path_1.default.join(decodedApkRoot, 'apktool.yml');
|
|
53
|
+
let apktoolYml = await promises_1.default.readFile(apktoolYmlPath, 'utf8');
|
|
54
|
+
apktoolYml = apktoolYml.replace(/^(\s+renameManifestPackage:\s+)(.+)$/gm, `$1${packageName}`);
|
|
55
|
+
await promises_1.default.writeFile(apktoolYmlPath, apktoolYml);
|
|
56
|
+
}
|
|
57
|
+
exports.addRenameManifestPackageAsync = addRenameManifestPackageAsync;
|
|
58
|
+
//#region Internals
|
|
59
|
+
async function getApktoolPathAsync() {
|
|
60
|
+
if (!cachedApktoolPath) {
|
|
61
|
+
const matches = await (0, glob_1.glob)('apktool_*.jar', {
|
|
62
|
+
cwd: node_path_1.default.join(__dirname, '../../assets'),
|
|
63
|
+
absolute: true,
|
|
64
|
+
});
|
|
65
|
+
cachedApktoolPath = matches[0];
|
|
66
|
+
(0, node_assert_1.default)(cachedApktoolPath, 'Unable to find the APKTool JAR file.');
|
|
67
|
+
}
|
|
68
|
+
return cachedApktoolPath;
|
|
69
|
+
}
|
|
70
|
+
async function addApktoolResourceAsync(decodedApkRoot, assetSet) {
|
|
71
|
+
const apktoolPublicXmlPath = node_path_1.default.join(decodedApkRoot, 'res/values/public.xml');
|
|
72
|
+
// [0] Retrieve the current max resource ID and the existing drawable names
|
|
73
|
+
const contents = await promises_1.default.readFile(apktoolPublicXmlPath, 'utf8');
|
|
74
|
+
const parser = new xml2js_1.Parser();
|
|
75
|
+
const existingNameSet = new Set();
|
|
76
|
+
const xmlJs = await parser.parseStringPromise(contents);
|
|
77
|
+
let maxDrawableResId = 0;
|
|
78
|
+
let maxRawResId = 0;
|
|
79
|
+
let maxResId = 0;
|
|
80
|
+
for (const item of xmlJs.resources.public) {
|
|
81
|
+
const intId = parseInt(item.$.id, 16);
|
|
82
|
+
if (item.$.type === 'drawable') {
|
|
83
|
+
maxDrawableResId = Math.max(maxDrawableResId, intId);
|
|
84
|
+
existingNameSet.add(item.$.name);
|
|
85
|
+
}
|
|
86
|
+
else if (item.$.type === 'raw') {
|
|
87
|
+
maxRawResId = Math.max(maxRawResId, intId);
|
|
88
|
+
existingNameSet.add(item.$.name);
|
|
89
|
+
}
|
|
90
|
+
maxResId = Math.max(maxResId, intId);
|
|
91
|
+
}
|
|
92
|
+
// [1] Add new resources
|
|
93
|
+
(0, node_assert_1.default)(maxDrawableResId !== 0, 'Drawable resources must be defined.');
|
|
94
|
+
if (maxRawResId === 0) {
|
|
95
|
+
// If there are no raw resources, start a new resource IDs section.
|
|
96
|
+
// A resource ID section has 0x10000 bounary.
|
|
97
|
+
maxRawResId = (maxResId & 0xffff0000) + 0x10000;
|
|
98
|
+
}
|
|
99
|
+
const drawableResIdBoundary = (maxDrawableResId & 0xffff0000) + 0x10000;
|
|
100
|
+
const rawResIdBoundary = (maxRawResId & 0xffff0000) + 0x10000;
|
|
101
|
+
const newAssetSet = new Set([...assetSet].filter(({ name }) => !existingNameSet.has(name)));
|
|
102
|
+
for (const asset of newAssetSet) {
|
|
103
|
+
if (asset.type === 'drawable') {
|
|
104
|
+
maxDrawableResId += 1;
|
|
105
|
+
(0, node_assert_1.default)(maxDrawableResId < drawableResIdBoundary, 'Drawable resource ID boundary exceeded.');
|
|
106
|
+
xmlJs.resources.public.push({
|
|
107
|
+
$: { type: 'drawable', name: asset.name, id: `0x${maxDrawableResId.toString(16)}` },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else if (asset.type === 'raw') {
|
|
111
|
+
maxRawResId += 1;
|
|
112
|
+
(0, node_assert_1.default)(maxRawResId < rawResIdBoundary, 'Raw resource ID boundary exceeded.');
|
|
113
|
+
xmlJs.resources.public.push({
|
|
114
|
+
$: { type: 'raw', name: asset.name, id: `0x${maxRawResId.toString(16)}` },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// [2] Write the updated public.xml file
|
|
119
|
+
const builder = new xml2js_1.Builder();
|
|
120
|
+
const xml = builder.buildObject(xmlJs);
|
|
121
|
+
await promises_1.default.writeFile(apktoolPublicXmlPath, xml);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns the asset type based on the name.
|
|
125
|
+
*/
|
|
126
|
+
function getAssetType(name) {
|
|
127
|
+
if (name.startsWith('drawable'))
|
|
128
|
+
return 'drawable';
|
|
129
|
+
if (name === 'raw')
|
|
130
|
+
return 'raw';
|
|
131
|
+
throw new Error(`Unsupported asset type for ${name}`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Copies files and directories from src to dest while maintaining the nested structure (up to the specified level).
|
|
135
|
+
* @param src The source folder path.
|
|
136
|
+
* @param dest The destination folder path.
|
|
137
|
+
* @param level The current depth level (starts at 0).
|
|
138
|
+
* @param maxLevel The maximum depth level to process.
|
|
139
|
+
*/
|
|
140
|
+
async function copyAssetsAsync(src, dest, level = 0, maxLevel = 2, assetType = undefined, assetSet = new Set()) {
|
|
141
|
+
await promises_1.default.mkdir(dest, { recursive: true });
|
|
142
|
+
const entries = await promises_1.default.readdir(src, { withFileTypes: true });
|
|
143
|
+
await Promise.all(entries.map(async (entry) => {
|
|
144
|
+
const srcPath = node_path_1.default.join(src, entry.name);
|
|
145
|
+
const destPath = node_path_1.default.join(dest, entry.name);
|
|
146
|
+
if (entry.isDirectory()) {
|
|
147
|
+
const _assetType = assetType ?? getAssetType(entry.name);
|
|
148
|
+
if (level < maxLevel - 1) {
|
|
149
|
+
await copyAssetsAsync(srcPath, destPath, level + 1, maxLevel, _assetType, assetSet);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// When the maximum level is reached, copy the directory without further recursion
|
|
153
|
+
await promises_1.default.mkdir(destPath, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
await promises_1.default.copyFile(srcPath, destPath);
|
|
158
|
+
const { name } = node_path_1.default.parse(entry.name);
|
|
159
|
+
(0, node_assert_1.default)(assetType, 'Asset type must be defined.');
|
|
160
|
+
assetSet.add({ name, type: assetType });
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
return assetSet;
|
|
164
|
+
}
|
|
165
|
+
//#endregion Internals
|
|
@@ -2,31 +2,24 @@ import type { AndroidSigningOptions, NormalizedOptions } from '../types';
|
|
|
2
2
|
export interface AndroidTools {
|
|
3
3
|
aapt2Path: string;
|
|
4
4
|
apksignerPath: string;
|
|
5
|
+
dexdumpPath: string;
|
|
5
6
|
zipalignPath: string;
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
9
|
* Get the paths to the Android build-tools.
|
|
9
10
|
*/
|
|
10
11
|
export declare function getAndroidBuildToolsAsync(options?: NormalizedOptions): Promise<AndroidTools>;
|
|
11
|
-
/**
|
|
12
|
-
* Create an APK with proto-based resources.
|
|
13
|
-
*/
|
|
14
|
-
export declare function createProtoBasedResourcesAsync(options: NormalizedOptions): Promise<{
|
|
15
|
-
androidManiestFilePath: string;
|
|
16
|
-
resourcesPbFilePath: string;
|
|
17
|
-
}>;
|
|
18
|
-
/**
|
|
19
|
-
* Create an APK with binary-based resources.
|
|
20
|
-
*/
|
|
21
|
-
export declare function createBinaryBasedResourcesAsync(options: NormalizedOptions): Promise<string>;
|
|
22
12
|
/**
|
|
23
13
|
* Create a resigned & updated APK.
|
|
24
|
-
* @param
|
|
25
|
-
* @param appConfigPath The path to the app.config file returned from `generateAppConfigAsync()`.
|
|
14
|
+
* @param unzippedApkRoot The root directory of the unzipped APK working directory.
|
|
26
15
|
* @returns
|
|
27
16
|
*/
|
|
28
|
-
export declare function createResignedApkAsync(
|
|
17
|
+
export declare function createResignedApkAsync(unzippedApkRoot: string, options: NormalizedOptions, signingOptions: AndroidSigningOptions): Promise<string>;
|
|
29
18
|
/**
|
|
30
19
|
* Find the latest build-tools directory in the `ANDROID_SDK_ROOT` directory.
|
|
31
20
|
*/
|
|
32
21
|
export declare function findLatestBuildToolsDirAsync(): Promise<string | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Search for classes in the APK with the given app ID pattern passing to grep.
|
|
24
|
+
*/
|
|
25
|
+
export declare function searchDexClassesAsync(unzipApkRoot: string, grepAppIdPattern: string): Promise<Set<string>>;
|
|
@@ -3,9 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.searchDexClassesAsync = exports.findLatestBuildToolsDirAsync = exports.createResignedApkAsync = exports.getAndroidBuildToolsAsync = void 0;
|
|
7
7
|
const steps_1 = require("@expo/steps");
|
|
8
|
-
const glob_1 = require("glob");
|
|
9
8
|
const node_assert_1 = __importDefault(require("node:assert"));
|
|
10
9
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -20,79 +19,29 @@ async function getAndroidBuildToolsAsync(options) {
|
|
|
20
19
|
cachedAndroidTools = {
|
|
21
20
|
aapt2Path: node_path_1.default.join(androidBuildToolsDir, 'aapt2'),
|
|
22
21
|
apksignerPath: node_path_1.default.join(androidBuildToolsDir, 'apksigner'),
|
|
22
|
+
dexdumpPath: node_path_1.default.join(androidBuildToolsDir, 'dexdump'),
|
|
23
23
|
zipalignPath: node_path_1.default.join(androidBuildToolsDir, 'zipalign'),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
return cachedAndroidTools;
|
|
27
27
|
}
|
|
28
28
|
exports.getAndroidBuildToolsAsync = getAndroidBuildToolsAsync;
|
|
29
|
-
/**
|
|
30
|
-
* Create an APK with proto-based resources.
|
|
31
|
-
*/
|
|
32
|
-
async function createProtoBasedResourcesAsync(options) {
|
|
33
|
-
const { sourceAppPath, workingDirectory } = options;
|
|
34
|
-
const { aapt2Path } = await getAndroidBuildToolsAsync(options);
|
|
35
|
-
const protoApkPath = node_path_1.default.join(workingDirectory, 'proto.apk');
|
|
36
|
-
await (0, steps_1.spawnAsync)(aapt2Path, ['convert', '-o', protoApkPath, '--output-format', 'proto', '--keep-raw-values', sourceAppPath], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
37
|
-
await (0, steps_1.spawnAsync)('unzip', [protoApkPath, '-d', workingDirectory], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
38
|
-
const removeFiles = await (0, glob_1.glob)('**/*', {
|
|
39
|
-
cwd: workingDirectory,
|
|
40
|
-
maxDepth: 1,
|
|
41
|
-
ignore: ['resources.pb', 'AndroidManifest.xml', 'res/**'],
|
|
42
|
-
absolute: true,
|
|
43
|
-
});
|
|
44
|
-
await Promise.all(removeFiles.map((file) => promises_1.default.rm(file, { recursive: true })));
|
|
45
|
-
return {
|
|
46
|
-
androidManiestFilePath: node_path_1.default.join(workingDirectory, 'AndroidManifest.xml'),
|
|
47
|
-
resourcesPbFilePath: node_path_1.default.join(workingDirectory, 'resources.pb'),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
exports.createProtoBasedResourcesAsync = createProtoBasedResourcesAsync;
|
|
51
|
-
/**
|
|
52
|
-
* Create an APK with binary-based resources.
|
|
53
|
-
*/
|
|
54
|
-
async function createBinaryBasedResourcesAsync(options) {
|
|
55
|
-
const { workingDirectory } = options;
|
|
56
|
-
const { aapt2Path } = await getAndroidBuildToolsAsync(options);
|
|
57
|
-
const protoUpdatedApkPath = node_path_1.default.join(workingDirectory, 'proto-updated.apk');
|
|
58
|
-
const binaryApkPath = node_path_1.default.join(workingDirectory, 'binary.apk');
|
|
59
|
-
await (0, steps_1.spawnAsync)('zip', ['-r', '-0', protoUpdatedApkPath, 'resources.pb', 'AndroidManifest.xml', 'res'], options.verbose
|
|
60
|
-
? { logger: options.logger, stdio: 'pipe', cwd: workingDirectory }
|
|
61
|
-
: { cwd: workingDirectory });
|
|
62
|
-
await (0, steps_1.spawnAsync)(aapt2Path, [
|
|
63
|
-
'convert',
|
|
64
|
-
'-o',
|
|
65
|
-
binaryApkPath,
|
|
66
|
-
'--output-format',
|
|
67
|
-
'binary',
|
|
68
|
-
'--keep-raw-values',
|
|
69
|
-
protoUpdatedApkPath,
|
|
70
|
-
], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
71
|
-
return binaryApkPath;
|
|
72
|
-
}
|
|
73
|
-
exports.createBinaryBasedResourcesAsync = createBinaryBasedResourcesAsync;
|
|
74
29
|
/**
|
|
75
30
|
* Create a resigned & updated APK.
|
|
76
|
-
* @param
|
|
77
|
-
* @param appConfigPath The path to the app.config file returned from `generateAppConfigAsync()`.
|
|
31
|
+
* @param unzippedApkRoot The root directory of the unzipped APK working directory.
|
|
78
32
|
* @returns
|
|
79
33
|
*/
|
|
80
|
-
async function createResignedApkAsync(
|
|
34
|
+
async function createResignedApkAsync(unzippedApkRoot, options, signingOptions) {
|
|
81
35
|
const { workingDirectory } = options;
|
|
82
36
|
const { apksignerPath, zipalignPath } = await getAndroidBuildToolsAsync(options);
|
|
83
|
-
const unzipWorkingDirectory = node_path_1.default.join(workingDirectory, 'unzip');
|
|
84
37
|
const resignedApkPath = node_path_1.default.join(workingDirectory, 'resigned.apk');
|
|
85
|
-
const resignedApkUnalignedPath = node_path_1.default.
|
|
86
|
-
await promises_1.default.mkdir(unzipWorkingDirectory, { recursive: true });
|
|
87
|
-
await (0, steps_1.spawnAsync)('unzip', [options.sourceAppPath, '-d', unzipWorkingDirectory], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
88
|
-
await (0, steps_1.spawnAsync)('unzip', ['-o', binaryApkPath, '-d', unzipWorkingDirectory], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
89
|
-
await promises_1.default.copyFile(appConfigPath, node_path_1.default.join(unzipWorkingDirectory, 'assets', 'app.config'));
|
|
38
|
+
const resignedApkUnalignedPath = node_path_1.default.resolve(workingDirectory, 'resigned-unaligned.apk');
|
|
90
39
|
await (0, steps_1.spawnAsync)('zip', ['-r', '-0', resignedApkUnalignedPath, 'lib', 'resources.arsc'], options.verbose
|
|
91
|
-
? { logger: options.logger, stdio: 'pipe', cwd:
|
|
92
|
-
: { cwd:
|
|
40
|
+
? { logger: options.logger, stdio: 'pipe', cwd: unzippedApkRoot }
|
|
41
|
+
: { cwd: unzippedApkRoot });
|
|
93
42
|
await (0, steps_1.spawnAsync)('zip', ['-r', resignedApkUnalignedPath, '.', '-x', 'lib/*', '-x', 'resources.arsc'], options.verbose
|
|
94
|
-
? { logger: options.logger, stdio: 'pipe', cwd:
|
|
95
|
-
: { cwd:
|
|
43
|
+
? { logger: options.logger, stdio: 'pipe', cwd: unzippedApkRoot }
|
|
44
|
+
: { cwd: unzippedApkRoot });
|
|
96
45
|
await (0, steps_1.spawnAsync)(zipalignPath, ['-v', '-p', '4', resignedApkUnalignedPath, resignedApkPath], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
97
46
|
const signerArgs = [
|
|
98
47
|
'sign',
|
|
@@ -149,3 +98,20 @@ async function findLatestBuildToolsDirAsync() {
|
|
|
149
98
|
}
|
|
150
99
|
}
|
|
151
100
|
exports.findLatestBuildToolsDirAsync = findLatestBuildToolsDirAsync;
|
|
101
|
+
/**
|
|
102
|
+
* Search for classes in the APK with the given app ID pattern passing to grep.
|
|
103
|
+
*/
|
|
104
|
+
async function searchDexClassesAsync(unzipApkRoot, grepAppIdPattern) {
|
|
105
|
+
const { dexdumpPath } = await getAndroidBuildToolsAsync();
|
|
106
|
+
const grepPattern = `"^ Class descriptor : 'L${grepAppIdPattern.replace(/\./g, '/')}\\/"`;
|
|
107
|
+
const { stdout } = await (0, steps_1.spawnAsync)(dexdumpPath, ['classes*.dex', '|', 'grep', grepPattern], {
|
|
108
|
+
cwd: unzipApkRoot,
|
|
109
|
+
shell: true,
|
|
110
|
+
});
|
|
111
|
+
const classes = stdout
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((line) => line.match(/Class descriptor\s+?:\s+?'L(.+);'/)?.[1].replace(/\//g, '.'))
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
return new Set(classes);
|
|
116
|
+
}
|
|
117
|
+
exports.searchDexClassesAsync = searchDexClassesAsync;
|
package/build/android/index.js
CHANGED
|
@@ -5,9 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.repackAppAndroidAsync = void 0;
|
|
7
7
|
const config_1 = require("@expo/config");
|
|
8
|
+
const steps_1 = require("@expo/steps");
|
|
9
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
10
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
12
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
13
|
+
const apktool_1 = require("./apktool");
|
|
11
14
|
const build_tools_1 = require("./build-tools");
|
|
12
15
|
const resources_1 = require("./resources");
|
|
13
16
|
const expo_1 = require("../expo");
|
|
@@ -23,27 +26,55 @@ async function repackAppAndroidAsync(_options) {
|
|
|
23
26
|
isPublicConfig: true,
|
|
24
27
|
skipSDKVersionRequirement: true,
|
|
25
28
|
});
|
|
26
|
-
const
|
|
29
|
+
const decodedApkRoot = await (0, apktool_1.decodeApkAsync)(node_path_1.default.resolve(options.sourceAppPath), options);
|
|
30
|
+
(0, node_assert_1.default)(exp.android?.package, 'Expected app ID (`android.package`) to be defined in app.json');
|
|
31
|
+
const updatesRuntimeVersion = (0, expo_1.isExpoUpdatesInstalled)(options.projectRoot)
|
|
32
|
+
? await (0, expo_1.resolveRuntimeVersionAsync)(options, exp)
|
|
33
|
+
: null;
|
|
27
34
|
logger.info(picocolors_1.default.dim(`Resolved runtime version: ${updatesRuntimeVersion}`));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
if (options.exportEmbedOptions != null) {
|
|
36
|
+
logger.info(`Generating bundle`);
|
|
37
|
+
const { assetRoot, bundleOutputPath } = await (0, expo_1.generateBundleAssetsAsync)(exp, options);
|
|
38
|
+
logger.info(`Finished generating bundle ✅`);
|
|
39
|
+
logger.info(`Adding bundled resources`);
|
|
40
|
+
await (0, apktool_1.addBundleAssetsToDecodedApkAsync)({
|
|
41
|
+
decodedApkRoot,
|
|
42
|
+
assetRoot,
|
|
43
|
+
bundleOutputPath,
|
|
44
|
+
});
|
|
45
|
+
logger.info(`Finished adding bundled resources ✅`);
|
|
46
|
+
}
|
|
31
47
|
logger.info(`Updating Androidmanifest.xml`);
|
|
32
|
-
|
|
48
|
+
const androidManiestFilePath = node_path_1.default.join(decodedApkRoot, 'AndroidManifest.xml');
|
|
49
|
+
const androidManiestXml = await (0, resources_1.parseXmlFileAsync)(androidManiestFilePath);
|
|
50
|
+
const originalAppId = await (0, resources_1.queryAppIdFromManifestAsync)(androidManiestXml);
|
|
51
|
+
const dexClasses = await (0, build_tools_1.searchDexClassesAsync)(decodedApkRoot, originalAppId);
|
|
52
|
+
await (0, resources_1.updateAndroidManifestAsync)({
|
|
53
|
+
config: exp,
|
|
54
|
+
androidManiestXml,
|
|
55
|
+
dexClasses,
|
|
56
|
+
originalAppId,
|
|
57
|
+
options,
|
|
58
|
+
updatesRuntimeVersion,
|
|
59
|
+
});
|
|
60
|
+
await (0, apktool_1.addRenameManifestPackageAsync)(decodedApkRoot, exp.android?.package);
|
|
61
|
+
await (0, resources_1.buildXmlFileAsync)(androidManiestXml, androidManiestFilePath);
|
|
33
62
|
logger.info(`Finished updating Androidmanifest.xml ✅`);
|
|
34
|
-
logger.info(`Updating resources
|
|
35
|
-
await (0, resources_1.updateResourcesAsync)(exp,
|
|
36
|
-
logger.info(`Finished updating resources
|
|
37
|
-
logger.info(`Creating binary based resources`);
|
|
38
|
-
const binaryApkPath = await (0, build_tools_1.createBinaryBasedResourcesAsync)(options);
|
|
39
|
-
logger.info(`Finished creating binary based resources ✅`);
|
|
63
|
+
logger.info(`Updating resources`);
|
|
64
|
+
await (0, resources_1.updateResourcesAsync)({ config: exp, decodedApkRoot });
|
|
65
|
+
logger.info(`Finished updating resources ✅`);
|
|
40
66
|
logger.info(`Generating app config`);
|
|
41
67
|
const appConfigPath = await (0, expo_1.generateAppConfigAsync)(options, exp);
|
|
68
|
+
await promises_1.default.copyFile(appConfigPath, node_path_1.default.join(decodedApkRoot, 'assets', 'app.config'));
|
|
42
69
|
logger.info(`Finished generating app config ✅`);
|
|
43
70
|
logger.info(`Creating updated apk`);
|
|
44
|
-
const
|
|
71
|
+
const apktoolPackedApkPath = await (0, apktool_1.rebuildApkAsync)(decodedApkRoot, options);
|
|
72
|
+
const unzippedApkRoot = node_path_1.default.join(workingDirectory, 'unzip');
|
|
73
|
+
await promises_1.default.mkdir(unzippedApkRoot, { recursive: true });
|
|
74
|
+
await (0, steps_1.spawnAsync)('unzip', ['-o', apktoolPackedApkPath, '-d', unzippedApkRoot], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
|
|
75
|
+
const outputApk = await (0, build_tools_1.createResignedApkAsync)(unzippedApkRoot, options, {
|
|
45
76
|
keyStorePath: options.androidSigningOptions?.keyStorePath ??
|
|
46
|
-
node_path_1.default.resolve(__dirname, '
|
|
77
|
+
node_path_1.default.resolve(__dirname, '../../assets/debug.keystore'),
|
|
47
78
|
keyStorePassword: options.androidSigningOptions?.keyStorePassword ?? 'android',
|
|
48
79
|
keyAlias: options.androidSigningOptions?.keyAlias,
|
|
49
80
|
keyPassword: options.androidSigningOptions?.keyPassword,
|
|
@@ -52,7 +83,7 @@ async function repackAppAndroidAsync(_options) {
|
|
|
52
83
|
await promises_1.default.rename(outputApk, options.outputPath);
|
|
53
84
|
if (!options.skipWorkingDirCleanup) {
|
|
54
85
|
try {
|
|
55
|
-
await promises_1.default.
|
|
86
|
+
await promises_1.default.rm(workingDirectory, { recursive: true });
|
|
56
87
|
}
|
|
57
88
|
catch { }
|
|
58
89
|
}
|
|
@@ -1,10 +1,61 @@
|
|
|
1
1
|
import { type ExpoConfig } from '@expo/config';
|
|
2
2
|
import type { NormalizedOptions } from '../types';
|
|
3
|
+
export interface XmlNode {
|
|
4
|
+
$?: {
|
|
5
|
+
[key: string]: string;
|
|
6
|
+
};
|
|
7
|
+
_?: string;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}
|
|
10
|
+
export interface AndroidManifestType {
|
|
11
|
+
manifest: {
|
|
12
|
+
$: {
|
|
13
|
+
package: string;
|
|
14
|
+
};
|
|
15
|
+
application: {
|
|
16
|
+
$?: Record<string, string>;
|
|
17
|
+
'meta-data'?: XmlNode[];
|
|
18
|
+
activity?: {
|
|
19
|
+
$?: Record<string, string>;
|
|
20
|
+
'intent-filter'?: {
|
|
21
|
+
action?: {
|
|
22
|
+
$: Record<string, string>;
|
|
23
|
+
}[];
|
|
24
|
+
category?: {
|
|
25
|
+
$: Record<string, string>;
|
|
26
|
+
}[];
|
|
27
|
+
}[];
|
|
28
|
+
}[];
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
3
32
|
/**
|
|
4
|
-
* Update resources
|
|
33
|
+
* Update resources in the decoded APK.
|
|
5
34
|
*/
|
|
6
|
-
export declare function updateResourcesAsync(config
|
|
35
|
+
export declare function updateResourcesAsync({ config, decodedApkRoot, }: {
|
|
36
|
+
config: ExpoConfig;
|
|
37
|
+
decodedApkRoot: string;
|
|
38
|
+
}): Promise<void>;
|
|
7
39
|
/**
|
|
8
40
|
* Update the proto-based AndroidManiest.xml.
|
|
9
41
|
*/
|
|
10
|
-
export declare function updateAndroidManifestAsync(config
|
|
42
|
+
export declare function updateAndroidManifestAsync({ config, androidManiestXml, dexClasses, originalAppId, updatesRuntimeVersion, }: {
|
|
43
|
+
config: ExpoConfig;
|
|
44
|
+
androidManiestXml: AndroidManifestType;
|
|
45
|
+
dexClasses: Set<string>;
|
|
46
|
+
originalAppId: string;
|
|
47
|
+
options: NormalizedOptions;
|
|
48
|
+
updatesRuntimeVersion: string | null;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Parse the XML file.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseXmlFileAsync<T = XmlNode>(filePath: string): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* Build the XML file from the given node.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildXmlFileAsync(rootNode: XmlNode, filePath: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Query the app ID from the AndroidManiest.xml.
|
|
60
|
+
*/
|
|
61
|
+
export declare function queryAppIdFromManifestAsync(androidManiestXml: XmlNode): Promise<string>;
|