@expo/repack-app 0.1.10 → 0.1.11
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/README.md +110 -0
- package/bin/cli.js +98 -2
- package/{build/types.d.ts → dist/index.d.ts} +218 -143
- package/dist/index.js +34 -0
- package/package.json +23 -19
- package/build/android/apktool.d.ts +0 -21
- package/build/android/apktool.js +0 -175
- package/build/android/build-tools.d.ts +0 -25
- package/build/android/build-tools.js +0 -122
- package/build/android/index.d.ts +0 -5
- package/build/android/index.js +0 -104
- package/build/android/resources.d.ts +0 -69
- package/build/android/resources.js +0 -198
- package/build/cli.d.ts +0 -1
- package/build/cli.js +0 -95
- package/build/expo.d.ts +0 -30
- package/build/expo.js +0 -144
- package/build/index.d.ts +0 -3
- package/build/index.js +0 -22
- package/build/ios/build-tools.d.ts +0 -22
- package/build/ios/build-tools.js +0 -110
- package/build/ios/index.d.ts +0 -9
- package/build/ios/index.js +0 -109
- package/build/ios/resources.d.ts +0 -27
- package/build/ios/resources.js +0 -112
- package/build/ios/xcode-utils.d.ts +0 -4
- package/build/ios/xcode-utils.js +0 -20
- package/build/types.js +0 -3
- package/build/utils.d.ts +0 -25
- package/build/utils.js +0 -86
- /package/{assets → dist}/apktool_2.10.0.jar +0 -0
- /package/{assets → dist}/debug.keystore +0 -0
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/repack-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Repacking tool for Expo apps",
|
|
5
|
-
"main": "
|
|
6
|
-
"
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"typings": "dist/index.d.ts",
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "
|
|
10
|
-
"
|
|
9
|
+
"build:js": "./scripts/build.js",
|
|
10
|
+
"build:types": "tsc && api-extractor run",
|
|
11
|
+
"build:all": "bun run build:js && bun run build:types",
|
|
12
|
+
"clean": "rm -rf build dist",
|
|
11
13
|
"lint": "expo-module lint",
|
|
12
|
-
"prepare": "expo-module clean &&
|
|
14
|
+
"prepare": "expo-module prepare && bun run clean && bun run build:all",
|
|
13
15
|
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
16
|
"test": "expo-module test",
|
|
15
17
|
"typecheck": "expo-module typecheck"
|
|
@@ -18,33 +20,35 @@
|
|
|
18
20
|
"repack-app": "bin/cli.js"
|
|
19
21
|
},
|
|
20
22
|
"files": [
|
|
21
|
-
"assets",
|
|
22
23
|
"bin",
|
|
23
|
-
"
|
|
24
|
+
"dist"
|
|
24
25
|
],
|
|
25
26
|
"author": "650 Industries, Inc.",
|
|
26
27
|
"license": "BUSL-1.1",
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@expo/plist": "^0.2.0",
|
|
29
|
-
"@expo/spawn-async": "^1.7.2",
|
|
30
29
|
"commander": "^13.0.0",
|
|
31
|
-
"
|
|
32
|
-
"picocolors": "^1.1.1",
|
|
33
|
-
"resolve-from": "^5.0.0",
|
|
34
|
-
"slugify": "^1.6.6",
|
|
35
|
-
"temp-dir": "^2.0.0",
|
|
36
|
-
"xml2js": "^0.6.2"
|
|
30
|
+
"picocolors": "^1.1.1"
|
|
37
31
|
},
|
|
38
32
|
"devDependencies": {
|
|
39
33
|
"@expo/config": "^10.0.6",
|
|
34
|
+
"@expo/plist": "^0.2.0",
|
|
35
|
+
"@expo/spawn-async": "^1.7.2",
|
|
36
|
+
"@microsoft/api-extractor": "^7.52.8",
|
|
40
37
|
"@tsconfig/node22": "^22.0.0",
|
|
41
38
|
"@types/xml2js": "^0.4.14",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
39
|
+
"ajv": "^8.17.1",
|
|
40
|
+
"esbuild": "^0.25.4",
|
|
41
|
+
"eslint": "^9.24.0",
|
|
42
|
+
"expo-module-scripts": "^4.1.7",
|
|
43
|
+
"glob": "^11.0.0",
|
|
44
44
|
"jest": "^29.7.0",
|
|
45
45
|
"jest-snapshot-prettier": "npm:prettier@^3.4.2",
|
|
46
|
+
"resolve-from": "^5.0.0",
|
|
47
|
+
"slugify": "^1.6.6",
|
|
48
|
+
"temp-dir": "^2.0.0",
|
|
46
49
|
"ts-jest": "^29.2.5",
|
|
47
|
-
"typescript": "^5.
|
|
50
|
+
"typescript": "^5.8.3",
|
|
51
|
+
"xml2js": "^0.6.2"
|
|
48
52
|
},
|
|
49
53
|
"volta": {
|
|
50
54
|
"node": "22.12.0"
|
|
@@ -1,21 +0,0 @@
|
|
|
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>;
|
package/build/android/apktool.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
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.decodeApkAsync = decodeApkAsync;
|
|
7
|
-
exports.rebuildApkAsync = rebuildApkAsync;
|
|
8
|
-
exports.addBundleAssetsToDecodedApkAsync = addBundleAssetsToDecodedApkAsync;
|
|
9
|
-
exports.addRenameManifestPackageAsync = addRenameManifestPackageAsync;
|
|
10
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
-
const glob_1 = require("glob");
|
|
12
|
-
const node_assert_1 = __importDefault(require("node:assert"));
|
|
13
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
-
const xml2js_1 = require("xml2js");
|
|
15
|
-
let cachedApktoolPath = null;
|
|
16
|
-
/**
|
|
17
|
-
* Decode the APK file using APKTool.
|
|
18
|
-
*/
|
|
19
|
-
async function decodeApkAsync(apkPath, options) {
|
|
20
|
-
const { spawnAsync, workingDirectory } = options;
|
|
21
|
-
const apktoolPath = await getApktoolPathAsync();
|
|
22
|
-
const outputPath = 'decoded-apk';
|
|
23
|
-
await spawnAsync('java', ['-jar', apktoolPath, 'decode', apkPath, '-s', '-o', outputPath], {
|
|
24
|
-
cwd: workingDirectory,
|
|
25
|
-
});
|
|
26
|
-
return node_path_1.default.join(workingDirectory, outputPath);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Rebuild the decoded APK file using APKTool.
|
|
30
|
-
*/
|
|
31
|
-
async function rebuildApkAsync(decodedApkRoot, options) {
|
|
32
|
-
const { spawnAsync, workingDirectory } = options;
|
|
33
|
-
const apktoolPath = await getApktoolPathAsync();
|
|
34
|
-
const apktoolPackedApkName = 'apktool-packed.apk';
|
|
35
|
-
const apktoolPackedApkPath = node_path_1.default.resolve(workingDirectory, apktoolPackedApkName);
|
|
36
|
-
await spawnAsync('java', [
|
|
37
|
-
'-jar',
|
|
38
|
-
apktoolPath,
|
|
39
|
-
'build',
|
|
40
|
-
'-o',
|
|
41
|
-
apktoolPackedApkPath,
|
|
42
|
-
'--use-aapt2',
|
|
43
|
-
decodedApkRoot,
|
|
44
|
-
]);
|
|
45
|
-
return apktoolPackedApkPath;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Add the bundle assets to the decoded APK.
|
|
49
|
-
*/
|
|
50
|
-
async function addBundleAssetsToDecodedApkAsync({ decodedApkRoot, assetRoot, bundleOutputPath, }) {
|
|
51
|
-
await promises_1.default.copyFile(bundleOutputPath, node_path_1.default.join(decodedApkRoot, 'assets', 'index.android.bundle'));
|
|
52
|
-
const assetSet = await copyAssetsAsync(assetRoot, node_path_1.default.join(decodedApkRoot, 'res'));
|
|
53
|
-
await addApktoolResourceAsync(decodedApkRoot, assetSet);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Update the apktool.yml `renameManifestPackage`
|
|
57
|
-
*/
|
|
58
|
-
async function addRenameManifestPackageAsync(decodedApkRoot, packageName) {
|
|
59
|
-
const apktoolYmlPath = node_path_1.default.join(decodedApkRoot, 'apktool.yml');
|
|
60
|
-
let apktoolYml = await promises_1.default.readFile(apktoolYmlPath, 'utf8');
|
|
61
|
-
apktoolYml = apktoolYml.replace(/^(\s+renameManifestPackage:\s+)(.+)$/gm, `$1${packageName}`);
|
|
62
|
-
await promises_1.default.writeFile(apktoolYmlPath, apktoolYml);
|
|
63
|
-
}
|
|
64
|
-
//#region Internals
|
|
65
|
-
async function getApktoolPathAsync() {
|
|
66
|
-
if (!cachedApktoolPath) {
|
|
67
|
-
const matches = await (0, glob_1.glob)('apktool_*.jar', {
|
|
68
|
-
cwd: node_path_1.default.join(__dirname, '../../assets'),
|
|
69
|
-
absolute: true,
|
|
70
|
-
});
|
|
71
|
-
cachedApktoolPath = matches[0];
|
|
72
|
-
(0, node_assert_1.default)(cachedApktoolPath, 'Unable to find the APKTool JAR file.');
|
|
73
|
-
}
|
|
74
|
-
return cachedApktoolPath;
|
|
75
|
-
}
|
|
76
|
-
async function addApktoolResourceAsync(decodedApkRoot, assetMap) {
|
|
77
|
-
const apktoolPublicXmlPath = node_path_1.default.join(decodedApkRoot, 'res/values/public.xml');
|
|
78
|
-
// [0] Retrieve the current max resource ID and the existing drawable names
|
|
79
|
-
const contents = await promises_1.default.readFile(apktoolPublicXmlPath, 'utf8');
|
|
80
|
-
const parser = new xml2js_1.Parser();
|
|
81
|
-
const existingNameSet = new Set();
|
|
82
|
-
const xmlJs = await parser.parseStringPromise(contents);
|
|
83
|
-
let maxDrawableResId = 0;
|
|
84
|
-
let maxRawResId = 0;
|
|
85
|
-
let maxResId = 0;
|
|
86
|
-
for (const item of xmlJs.resources.public) {
|
|
87
|
-
const intId = parseInt(item.$.id, 16);
|
|
88
|
-
if (item.$.type === 'drawable') {
|
|
89
|
-
maxDrawableResId = Math.max(maxDrawableResId, intId);
|
|
90
|
-
existingNameSet.add(item.$.name);
|
|
91
|
-
}
|
|
92
|
-
else if (item.$.type === 'raw') {
|
|
93
|
-
maxRawResId = Math.max(maxRawResId, intId);
|
|
94
|
-
existingNameSet.add(item.$.name);
|
|
95
|
-
}
|
|
96
|
-
maxResId = Math.max(maxResId, intId);
|
|
97
|
-
}
|
|
98
|
-
// [1] Add new resources
|
|
99
|
-
(0, node_assert_1.default)(maxDrawableResId !== 0, 'Drawable resources must be defined.');
|
|
100
|
-
if (maxRawResId === 0) {
|
|
101
|
-
// If there are no raw resources, start a new resource IDs section.
|
|
102
|
-
// A resource ID section has 0x10000 bounary.
|
|
103
|
-
maxRawResId = (maxResId & 0xffff0000) + 0x10000;
|
|
104
|
-
}
|
|
105
|
-
const drawableResIdBoundary = (maxDrawableResId & 0xffff0000) + 0x10000;
|
|
106
|
-
const rawResIdBoundary = (maxRawResId & 0xffff0000) + 0x10000;
|
|
107
|
-
const newAssetNameSet = new Set([...assetMap.keys()].filter((name) => !existingNameSet.has(name)));
|
|
108
|
-
for (const assetName of newAssetNameSet) {
|
|
109
|
-
const asset = assetMap.get(assetName);
|
|
110
|
-
if (asset == null) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (asset.type === 'drawable') {
|
|
114
|
-
maxDrawableResId += 1;
|
|
115
|
-
(0, node_assert_1.default)(maxDrawableResId < drawableResIdBoundary, 'Drawable resource ID boundary exceeded.');
|
|
116
|
-
xmlJs.resources.public.push({
|
|
117
|
-
$: { type: 'drawable', name: asset.name, id: `0x${maxDrawableResId.toString(16)}` },
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
else if (asset.type === 'raw') {
|
|
121
|
-
maxRawResId += 1;
|
|
122
|
-
(0, node_assert_1.default)(maxRawResId < rawResIdBoundary, 'Raw resource ID boundary exceeded.');
|
|
123
|
-
xmlJs.resources.public.push({
|
|
124
|
-
$: { type: 'raw', name: asset.name, id: `0x${maxRawResId.toString(16)}` },
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// [2] Write the updated public.xml file
|
|
129
|
-
const builder = new xml2js_1.Builder();
|
|
130
|
-
const xml = builder.buildObject(xmlJs);
|
|
131
|
-
await promises_1.default.writeFile(apktoolPublicXmlPath, xml);
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Returns the asset type based on the name.
|
|
135
|
-
*/
|
|
136
|
-
function getAssetType(name) {
|
|
137
|
-
if (name.startsWith('drawable'))
|
|
138
|
-
return 'drawable';
|
|
139
|
-
if (name === 'raw')
|
|
140
|
-
return 'raw';
|
|
141
|
-
throw new Error(`Unsupported asset type for ${name}`);
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Copies files and directories from src to dest while maintaining the nested structure (up to the specified level).
|
|
145
|
-
* @param src The source folder path.
|
|
146
|
-
* @param dest The destination folder path.
|
|
147
|
-
* @param level The current depth level (starts at 0).
|
|
148
|
-
* @param maxLevel The maximum depth level to process.
|
|
149
|
-
*/
|
|
150
|
-
async function copyAssetsAsync(src, dest, level = 0, maxLevel = 2, assetType = undefined, assetMap = new Map()) {
|
|
151
|
-
await promises_1.default.mkdir(dest, { recursive: true });
|
|
152
|
-
const entries = await promises_1.default.readdir(src, { withFileTypes: true });
|
|
153
|
-
await Promise.all(entries.map(async (entry) => {
|
|
154
|
-
const srcPath = node_path_1.default.join(src, entry.name);
|
|
155
|
-
const destPath = node_path_1.default.join(dest, entry.name);
|
|
156
|
-
if (entry.isDirectory()) {
|
|
157
|
-
const _assetType = assetType ?? getAssetType(entry.name);
|
|
158
|
-
if (level < maxLevel - 1) {
|
|
159
|
-
await copyAssetsAsync(srcPath, destPath, level + 1, maxLevel, _assetType, assetMap);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
// When the maximum level is reached, copy the directory without further recursion
|
|
163
|
-
await promises_1.default.mkdir(destPath, { recursive: true });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
await promises_1.default.copyFile(srcPath, destPath);
|
|
168
|
-
const { name } = node_path_1.default.parse(entry.name);
|
|
169
|
-
(0, node_assert_1.default)(assetType, 'Asset type must be defined.');
|
|
170
|
-
assetMap.set(name, { name, type: assetType });
|
|
171
|
-
}
|
|
172
|
-
}));
|
|
173
|
-
return assetMap;
|
|
174
|
-
}
|
|
175
|
-
//#endregion Internals
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { AndroidSigningOptions, NormalizedOptions } from '../types';
|
|
2
|
-
export interface AndroidTools {
|
|
3
|
-
aapt2Path: string;
|
|
4
|
-
apksignerPath: string;
|
|
5
|
-
dexdumpPath: string;
|
|
6
|
-
zipalignPath: string;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Get the paths to the Android build-tools.
|
|
10
|
-
*/
|
|
11
|
-
export declare function getAndroidBuildToolsAsync(options: NormalizedOptions): Promise<AndroidTools>;
|
|
12
|
-
/**
|
|
13
|
-
* Create a resigned & updated APK.
|
|
14
|
-
* @param unzippedApkRoot The root directory of the unzipped APK working directory.
|
|
15
|
-
* @returns
|
|
16
|
-
*/
|
|
17
|
-
export declare function createResignedApkAsync(unzippedApkRoot: string, options: NormalizedOptions, signingOptions: AndroidSigningOptions): Promise<string>;
|
|
18
|
-
/**
|
|
19
|
-
* Find the latest build-tools directory in the `ANDROID_SDK_ROOT` directory.
|
|
20
|
-
*/
|
|
21
|
-
export declare function findLatestBuildToolsDirAsync(options: NormalizedOptions): 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, options: NormalizedOptions): Promise<Set<string> | null>;
|
|
@@ -1,122 +0,0 @@
|
|
|
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.getAndroidBuildToolsAsync = getAndroidBuildToolsAsync;
|
|
7
|
-
exports.createResignedApkAsync = createResignedApkAsync;
|
|
8
|
-
exports.findLatestBuildToolsDirAsync = findLatestBuildToolsDirAsync;
|
|
9
|
-
exports.searchDexClassesAsync = searchDexClassesAsync;
|
|
10
|
-
const node_assert_1 = __importDefault(require("node:assert"));
|
|
11
|
-
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
12
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
-
let cachedAndroidTools = null;
|
|
14
|
-
/**
|
|
15
|
-
* Get the paths to the Android build-tools.
|
|
16
|
-
*/
|
|
17
|
-
async function getAndroidBuildToolsAsync(options) {
|
|
18
|
-
if (cachedAndroidTools == null) {
|
|
19
|
-
const androidBuildToolsDir = options?.androidBuildToolsDir ?? (await findLatestBuildToolsDirAsync(options));
|
|
20
|
-
(0, node_assert_1.default)(androidBuildToolsDir != null, 'Unable to find the Android build-tools directory.');
|
|
21
|
-
cachedAndroidTools = {
|
|
22
|
-
aapt2Path: node_path_1.default.join(androidBuildToolsDir, 'aapt2'),
|
|
23
|
-
apksignerPath: node_path_1.default.join(androidBuildToolsDir, 'apksigner'),
|
|
24
|
-
dexdumpPath: node_path_1.default.join(androidBuildToolsDir, 'dexdump'),
|
|
25
|
-
zipalignPath: node_path_1.default.join(androidBuildToolsDir, 'zipalign'),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
return cachedAndroidTools;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Create a resigned & updated APK.
|
|
32
|
-
* @param unzippedApkRoot The root directory of the unzipped APK working directory.
|
|
33
|
-
* @returns
|
|
34
|
-
*/
|
|
35
|
-
async function createResignedApkAsync(unzippedApkRoot, options, signingOptions) {
|
|
36
|
-
const { spawnAsync, workingDirectory } = options;
|
|
37
|
-
const { apksignerPath, zipalignPath } = await getAndroidBuildToolsAsync(options);
|
|
38
|
-
const resignedApkPath = node_path_1.default.join(workingDirectory, 'resigned.apk');
|
|
39
|
-
const resignedApkUnalignedPath = node_path_1.default.resolve(workingDirectory, 'resigned-unaligned.apk');
|
|
40
|
-
await spawnAsync('zip', ['-r', '-0', resignedApkUnalignedPath, 'lib', 'resources.arsc'], {
|
|
41
|
-
cwd: unzippedApkRoot,
|
|
42
|
-
});
|
|
43
|
-
await spawnAsync('zip', ['-r', resignedApkUnalignedPath, '.', '-x', 'lib/*', '-x', 'resources.arsc'], { cwd: unzippedApkRoot });
|
|
44
|
-
await spawnAsync(zipalignPath, ['-v', '-p', '4', resignedApkUnalignedPath, resignedApkPath]);
|
|
45
|
-
const signerArgs = [
|
|
46
|
-
'sign',
|
|
47
|
-
'--ks',
|
|
48
|
-
signingOptions.keyStorePath,
|
|
49
|
-
'--ks-pass',
|
|
50
|
-
signingOptions.keyStorePassword,
|
|
51
|
-
];
|
|
52
|
-
if (signingOptions.keyAlias) {
|
|
53
|
-
signerArgs.push('--ks-key-alias', signingOptions.keyAlias);
|
|
54
|
-
}
|
|
55
|
-
if (signingOptions.keyPassword) {
|
|
56
|
-
signerArgs.push('--key-pass', signingOptions.keyPassword);
|
|
57
|
-
}
|
|
58
|
-
signerArgs.push(resignedApkPath);
|
|
59
|
-
await spawnAsync(apksignerPath, signerArgs);
|
|
60
|
-
return resignedApkPath;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Find the latest build-tools directory in the `ANDROID_SDK_ROOT` directory.
|
|
64
|
-
*/
|
|
65
|
-
async function findLatestBuildToolsDirAsync(options) {
|
|
66
|
-
const { logger } = options;
|
|
67
|
-
const androidSdkRoot = process.env['ANDROID_SDK_ROOT'];
|
|
68
|
-
(0, node_assert_1.default)(androidSdkRoot != null, 'ANDROID_SDK_ROOT environment variable is not set');
|
|
69
|
-
const buildToolsDir = node_path_1.default.join(androidSdkRoot, 'build-tools');
|
|
70
|
-
try {
|
|
71
|
-
const entries = await promises_1.default.readdir(buildToolsDir, { withFileTypes: true });
|
|
72
|
-
const dirs = entries
|
|
73
|
-
.filter((entry) => entry.isDirectory())
|
|
74
|
-
.map((dir) => dir.name)
|
|
75
|
-
.sort((a, b) => {
|
|
76
|
-
// Convert version strings to numbers and compare
|
|
77
|
-
const versionA = a.split('.').map(Number);
|
|
78
|
-
const versionB = b.split('.').map(Number);
|
|
79
|
-
for (let i = 0; i < Math.min(versionA.length, versionB.length); i++) {
|
|
80
|
-
if (versionA[i] !== versionB[i]) {
|
|
81
|
-
return versionB[i] - versionA[i];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return versionB.length - versionA.length;
|
|
85
|
-
});
|
|
86
|
-
if (dirs.length > 0) {
|
|
87
|
-
return node_path_1.default.join(buildToolsDir, dirs[0]);
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
logger.error('No build-tools directories found.');
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
logger.error(`Failed to read the build-tools directory: ${error}`);
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Search for classes in the APK with the given app ID pattern passing to grep.
|
|
101
|
-
*/
|
|
102
|
-
async function searchDexClassesAsync(unzipApkRoot, grepAppIdPattern, options) {
|
|
103
|
-
const { logger, spawnAsync } = options;
|
|
104
|
-
const { dexdumpPath } = await getAndroidBuildToolsAsync(options);
|
|
105
|
-
const grepPattern = `"^ Class descriptor : 'L${grepAppIdPattern.replace(/\./g, '/')}\\/"`;
|
|
106
|
-
try {
|
|
107
|
-
const { stdout } = await spawnAsync(dexdumpPath, ['classes*.dex', '|', 'grep', grepPattern], {
|
|
108
|
-
cwd: unzipApkRoot,
|
|
109
|
-
shell: true,
|
|
110
|
-
stdio: 'pipe',
|
|
111
|
-
});
|
|
112
|
-
const classes = stdout
|
|
113
|
-
.split('\n')
|
|
114
|
-
.map((line) => line.match(/Class descriptor\s+?:\s+?'L(.+);'/)?.[1].replace(/\//g, '.'))
|
|
115
|
-
.filter(Boolean);
|
|
116
|
-
return new Set(classes);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
logger.info(`Failed to search for classes in the APK - grepAppIdPattern[${grepAppIdPattern}]: ${error}`);
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
package/build/android/index.d.ts
DELETED
package/build/android/index.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
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.repackAppAndroidAsync = repackAppAndroidAsync;
|
|
7
|
-
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
-
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const picocolors_1 = __importDefault(require("picocolors"));
|
|
11
|
-
const apktool_1 = require("./apktool");
|
|
12
|
-
const build_tools_1 = require("./build-tools");
|
|
13
|
-
const resources_1 = require("./resources");
|
|
14
|
-
const expo_1 = require("../expo");
|
|
15
|
-
const utils_1 = require("../utils");
|
|
16
|
-
/**
|
|
17
|
-
* Repack an Android app.
|
|
18
|
-
*/
|
|
19
|
-
async function repackAppAndroidAsync(_options) {
|
|
20
|
-
const options = await (0, utils_1.normalizeOptionAsync)(_options);
|
|
21
|
-
const { logger, spawnAsync, workingDirectory } = options;
|
|
22
|
-
await promises_1.default.mkdir(workingDirectory, { recursive: true });
|
|
23
|
-
const { exp } = (0, expo_1.getExpoConfig)(options.projectRoot, {
|
|
24
|
-
isPublicConfig: true,
|
|
25
|
-
skipSDKVersionRequirement: true,
|
|
26
|
-
});
|
|
27
|
-
const decodedApkRoot = await (0, apktool_1.decodeApkAsync)(node_path_1.default.resolve(options.sourceAppPath), options);
|
|
28
|
-
(0, node_assert_1.default)(exp.android?.package, 'Expected app ID (`android.package`) to be defined in app.json');
|
|
29
|
-
const useExpoUpdates = (0, expo_1.isExpoUpdatesInstalled)(options.projectRoot);
|
|
30
|
-
const updatesRuntimeVersion = useExpoUpdates
|
|
31
|
-
? await (0, expo_1.resolveRuntimeVersionAsync)(options, exp)
|
|
32
|
-
: null;
|
|
33
|
-
logger.info(picocolors_1.default.dim(`Resolved runtime version: ${updatesRuntimeVersion}`));
|
|
34
|
-
if (options.exportEmbedOptions != null) {
|
|
35
|
-
logger.time(`Generating bundle`);
|
|
36
|
-
const { assetRoot, bundleOutputPath } = await (0, expo_1.generateBundleAssetsAsync)(exp, options);
|
|
37
|
-
logger.timeEnd(`Generating bundle`);
|
|
38
|
-
logger.time(`Adding bundled resources`);
|
|
39
|
-
await (0, apktool_1.addBundleAssetsToDecodedApkAsync)({
|
|
40
|
-
decodedApkRoot,
|
|
41
|
-
assetRoot,
|
|
42
|
-
bundleOutputPath,
|
|
43
|
-
});
|
|
44
|
-
logger.timeEnd(`Adding bundled resources`);
|
|
45
|
-
if (useExpoUpdates) {
|
|
46
|
-
logger.time(`Generating 'app.manifest' for expo-updates`);
|
|
47
|
-
const updateManifestFile = await (0, expo_1.generateUpdatesEmbeddedManifestAsync)(options);
|
|
48
|
-
const updateDirectory = node_path_1.default.join(decodedApkRoot, 'assets');
|
|
49
|
-
await promises_1.default.copyFile(updateManifestFile, node_path_1.default.join(updateDirectory, node_path_1.default.basename(updateManifestFile)));
|
|
50
|
-
logger.timeEnd(`Generating 'app.manifest' for expo-updates`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
logger.time(`Updating Androidmanifest.xml`);
|
|
54
|
-
const androidManiestFilePath = node_path_1.default.join(decodedApkRoot, 'AndroidManifest.xml');
|
|
55
|
-
const androidManiestXml = await (0, resources_1.parseXmlFileAsync)(androidManiestFilePath);
|
|
56
|
-
const originalAppId = (0, resources_1.queryAppIdFromManifest)(androidManiestXml);
|
|
57
|
-
const grepAppIdPatterns = new Set([originalAppId]);
|
|
58
|
-
const originalAppIdFallback = (0, resources_1.queryAppIdFallbackFromManifest)(androidManiestXml, originalAppId);
|
|
59
|
-
if (originalAppIdFallback != null) {
|
|
60
|
-
grepAppIdPatterns.add(originalAppIdFallback);
|
|
61
|
-
}
|
|
62
|
-
const dexClasses = await Promise.any(Array.from(grepAppIdPatterns).map((appId) => (0, build_tools_1.searchDexClassesAsync)(decodedApkRoot, appId, options)));
|
|
63
|
-
(0, node_assert_1.default)(dexClasses != null, 'Failed to search for dex classes with appId in the APK');
|
|
64
|
-
await (0, resources_1.updateAndroidManifestAsync)({
|
|
65
|
-
config: exp,
|
|
66
|
-
androidManiestXml,
|
|
67
|
-
dexClasses,
|
|
68
|
-
originalAppId,
|
|
69
|
-
originalAppIdFallback,
|
|
70
|
-
options,
|
|
71
|
-
updatesRuntimeVersion,
|
|
72
|
-
});
|
|
73
|
-
await (0, apktool_1.addRenameManifestPackageAsync)(decodedApkRoot, exp.android?.package);
|
|
74
|
-
await (0, resources_1.buildXmlFileAsync)(androidManiestXml, androidManiestFilePath);
|
|
75
|
-
logger.timeEnd(`Updating Androidmanifest.xml`);
|
|
76
|
-
logger.time(`Updating resources`);
|
|
77
|
-
await (0, resources_1.updateResourcesAsync)({ config: exp, decodedApkRoot });
|
|
78
|
-
logger.timeEnd(`Updating resources`);
|
|
79
|
-
logger.time(`Generating app config`);
|
|
80
|
-
const appConfigPath = await (0, expo_1.generateAppConfigAsync)(options, exp);
|
|
81
|
-
await promises_1.default.copyFile(appConfigPath, node_path_1.default.join(decodedApkRoot, 'assets', 'app.config'));
|
|
82
|
-
logger.timeEnd(`Generating app config`);
|
|
83
|
-
logger.time(`Creating updated apk`);
|
|
84
|
-
const apktoolPackedApkPath = await (0, apktool_1.rebuildApkAsync)(decodedApkRoot, options);
|
|
85
|
-
const unzippedApkRoot = node_path_1.default.join(workingDirectory, 'unzip');
|
|
86
|
-
await promises_1.default.mkdir(unzippedApkRoot, { recursive: true });
|
|
87
|
-
await spawnAsync('unzip', ['-o', apktoolPackedApkPath, '-d', unzippedApkRoot]);
|
|
88
|
-
const outputApk = await (0, build_tools_1.createResignedApkAsync)(unzippedApkRoot, options, {
|
|
89
|
-
keyStorePath: options.androidSigningOptions?.keyStorePath ??
|
|
90
|
-
node_path_1.default.resolve(__dirname, '../../assets/debug.keystore'),
|
|
91
|
-
keyStorePassword: options.androidSigningOptions?.keyStorePassword ?? 'android',
|
|
92
|
-
keyAlias: options.androidSigningOptions?.keyAlias,
|
|
93
|
-
keyPassword: options.androidSigningOptions?.keyPassword,
|
|
94
|
-
});
|
|
95
|
-
logger.timeEnd(`Creating updated apk`);
|
|
96
|
-
await promises_1.default.rename(outputApk, options.outputPath);
|
|
97
|
-
if (!options.skipWorkingDirCleanup) {
|
|
98
|
-
try {
|
|
99
|
-
await promises_1.default.rm(workingDirectory, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
catch { }
|
|
102
|
-
}
|
|
103
|
-
return options.outputPath;
|
|
104
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { type ExpoConfig } from '@expo/config';
|
|
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
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Update resources in the decoded APK.
|
|
34
|
-
*/
|
|
35
|
-
export declare function updateResourcesAsync({ config, decodedApkRoot, }: {
|
|
36
|
-
config: ExpoConfig;
|
|
37
|
-
decodedApkRoot: string;
|
|
38
|
-
}): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* Update the proto-based AndroidManiest.xml.
|
|
41
|
-
*/
|
|
42
|
-
export declare function updateAndroidManifestAsync({ config, androidManiestXml, dexClasses, originalAppId, originalAppIdFallback, updatesRuntimeVersion, }: {
|
|
43
|
-
config: ExpoConfig;
|
|
44
|
-
androidManiestXml: AndroidManifestType;
|
|
45
|
-
dexClasses: Set<string>;
|
|
46
|
-
originalAppId: string;
|
|
47
|
-
originalAppIdFallback: string | null;
|
|
48
|
-
options: NormalizedOptions;
|
|
49
|
-
updatesRuntimeVersion: string | null;
|
|
50
|
-
}): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* Parse the XML file.
|
|
53
|
-
*/
|
|
54
|
-
export declare function parseXmlFileAsync<T = XmlNode>(filePath: string): Promise<T>;
|
|
55
|
-
/**
|
|
56
|
-
* Build the XML file from the given node.
|
|
57
|
-
*/
|
|
58
|
-
export declare function buildXmlFileAsync(rootNode: XmlNode, filePath: string): Promise<void>;
|
|
59
|
-
/**
|
|
60
|
-
* Query the app ID from the AndroidManiest.xml.
|
|
61
|
-
*/
|
|
62
|
-
export declare function queryAppIdFromManifest(androidManiestXml: XmlNode): string;
|
|
63
|
-
/**
|
|
64
|
-
* Query the fallback original app ID from the AndroidManiest.xml.
|
|
65
|
-
* Sometimes if the app were using `appIdSuffix` or overriding `applicationId` from build variants,
|
|
66
|
-
* the app ID in the `package` attribute will not be the original app ID for dex files
|
|
67
|
-
* As a fallback, we use a heuristic to retrieve original app ID from application class name.
|
|
68
|
-
*/
|
|
69
|
-
export declare function queryAppIdFallbackFromManifest(androidManiestXml: XmlNode, appId: string): string | null;
|