xdl
Version:
The Expo Development Library
911 lines (819 loc) • 26.4 kB
Flow
// Copyright 2015-present 650 Industries. All rights reserved.
/**
* @flow
*/
;
const fs = require('fs-extra');
const path = require('path');
const JsonFile = require('@expo/json-file');
const replaceString = require('replace-string');
const _ = require('lodash');
const globby = require('globby');
import * as ExponentTools from './ExponentTools';
import StandaloneContext from './StandaloneContext';
const { getManifestAsync, saveUrlToPathAsync, spawnAsyncThrowError, spawnAsync } = ExponentTools;
function exponentDirectory() {
if (process.env.TURTLE_WORKING_DIR_PATH) {
return process.env.TURTLE_WORKING_DIR_PATH;
} else if (process.env.EXPO_UNIVERSE_DIR) {
return path.join(process.env.EXPO_UNIVERSE_DIR, 'exponent');
} else {
throw new Error(`Can't determine exponent directory`);
}
}
async function regexFileAsync(regex, replace, filename) {
let file = await fs.promise.readFile(filename);
let fileString = file.toString();
await fs.promise.writeFile(filename, fileString.replace(regex, replace));
}
// Matches sed /d behavior
async function deleteLinesInFileAsync(startRegex, endRegex, filename) {
let file = await fs.promise.readFile(filename);
let fileString = file.toString();
let lines = fileString.split(/\r?\n/);
let filteredLines = [];
let inDeleteRange = false;
for (let i = 0; i < lines.length; i++) {
if (lines[i].match(startRegex)) {
inDeleteRange = true;
}
if (!inDeleteRange) {
filteredLines.push(lines[i]);
}
if (inDeleteRange && lines[i].match(endRegex)) {
inDeleteRange = false;
}
}
await fs.promise.writeFile(filename, filteredLines.join('\n'));
}
function xmlWeirdAndroidEscape(original) {
let noAmps = replaceString(original, '&', '&');
let noLt = replaceString(noAmps, '<', '<');
let noGt = replaceString(noLt, '>', '>');
let noApos = replaceString(noGt, '"', '\\"');
return replaceString(noApos, "'", "\\'");
}
exports.updateAndroidShellAppAsync = async function updateAndroidShellAppAsync(args: any) {
let {
url,
sdkVersion,
androidPackage,
privateConfigFile,
keystore,
alias,
keystorePassword,
keyPassword,
releaseChannel,
outputFile,
} = args;
releaseChannel = releaseChannel ? releaseChannel : 'default';
let manifest = await getManifestAsync(url, {
'Exponent-SDK-Version': sdkVersion,
'Exponent-Platform': 'android',
'Expo-Release-Channel': releaseChannel,
});
let fullManifestUrl = `${url.replace('exp://', 'https://')}/index.exp`;
let bundleUrl = manifest.bundleUrl;
let shellPath = path.join(exponentDirectory(), 'android-shell-app');
await fs.remove(path.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app-manifest.json'));
await fs.writeFileSync(
path.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app-manifest.json'),
JSON.stringify(manifest)
);
await fs.remove(path.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app.bundle'));
await saveUrlToPathAsync(
bundleUrl,
path.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app.bundle')
);
await deleteLinesInFileAsync(
`START\ EMBEDDED\ RESPONSES`,
`END\ EMBEDDED\ RESPONSES`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
await regexFileAsync(
'// ADD EMBEDDED RESPONSES HERE',
`
// ADD EMBEDDED RESPONSES HERE
// START EMBEDDED RESPONSES
embeddedResponses.add(new EmbeddedResponse("${fullManifestUrl}", "assets://shell-app-manifest.json", "application/json"));
embeddedResponses.add(new EmbeddedResponse("${bundleUrl}", "assets://shell-app.bundle", "application/javascript"));
// END EMBEDDED RESPONSES`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
await regexFileAsync(
'RELEASE_CHANNEL = "default"',
`RELEASE_CHANNEL = "${releaseChannel}"`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
};
function backgroundImagesForApp(shellPath, manifest) {
// returns an array like:
// [
// {url: 'urlToDownload', path: 'pathToSaveTo'},
// {url: 'anotherURlToDownload', path: 'anotherPathToSaveTo'},
// ]
const imageKeys = ['ldpi', 'mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi'];
let basePath = path.join(shellPath, 'expoview', 'src', 'main', 'res');
if (_.get(manifest, 'android.splash')) {
var splash = _.get(manifest, 'android.splash');
return _.reduce(
imageKeys,
function(acc, imageKey) {
let url = _.get(splash, `${imageKey}Url`);
if (url) {
acc.push({
url: url,
path: path.join(basePath, `drawable-${imageKey}`, 'shell_launch_background_image.png'),
});
}
return acc;
},
[]
);
}
if (_.get(manifest, 'splash.imageUrl')) {
let url = _.get(manifest, 'splash.imageUrl');
return [
{
url: url,
path: path.join(basePath, 'drawable-xxxhdpi', 'shell_launch_background_image.png'),
},
];
}
return [];
}
function getSplashScreenBackgroundColor(manifest) {
let backgroundColor;
if (manifest.android && manifest.android.splash && manifest.android.splash.backgroundColor) {
backgroundColor = manifest.android.splash.backgroundColor;
} else if (manifest.splash && manifest.splash.backgroundColor) {
backgroundColor = manifest.splash.backgroundColor;
}
// Default to white
if (!backgroundColor) {
backgroundColor = '#FFFFFF';
}
return backgroundColor;
}
/*
if resizeMode is 'cover' we should show LoadingView:
using an ImageView, unlike having a BitmapDrawable
provides a fullscreen image without distortions
*/
function shouldShowLoadingView(manifest) {
return (
(manifest.android &&
manifest.android.splash &&
manifest.android.splash.resizeMode &&
manifest.android.splash.resizeMode === 'cover') ||
(manifest.splash && manifest.splash.resizeMode && manifest.splash.resizeMode === 'cover')
);
}
export async function copyInitialShellAppFilesAsync(androidSrcPath, shellPath) {
await spawnAsync(`../../tools-public/generate-dynamic-macros-android.sh`, [], {
stdio: 'inherit',
cwd: path.join(exponentDirectory(), 'android', 'app'),
}); // populate android template files now since we take out the prebuild step later on
let copyToShellApp = async fileName => {
await fs.copy(path.join(androidSrcPath, fileName), path.join(shellPath, fileName));
};
await copyToShellApp('expoview');
await copyToShellApp('ReactCommon');
await copyToShellApp('ReactAndroid');
await copyToShellApp('android.iml');
await copyToShellApp('app');
await copyToShellApp('build.gradle');
await copyToShellApp('gradle');
await copyToShellApp('gradle.properties');
await copyToShellApp('gradlew');
await copyToShellApp('local.properties');
await copyToShellApp('settings.gradle');
await copyToShellApp('maven');
}
exports.createAndroidShellAppAsync = async function createAndroidShellAppAsync(args: any) {
let {
url,
sdkVersion,
releaseChannel,
privateConfigFile,
configuration,
keystore,
alias,
keystorePassword,
keyPassword,
outputFile,
} = args;
let androidSrcPath = path.join(exponentDirectory(), 'android');
let shellPath = path.join(exponentDirectory(), 'android-shell-app');
await fs.remove(shellPath);
await fs.ensureDir(shellPath);
releaseChannel = releaseChannel ? releaseChannel : 'default';
let manifest = await getManifestAsync(url, {
'Exponent-SDK-Version': sdkVersion,
'Exponent-Platform': 'android',
'Expo-Release-Channel': releaseChannel,
});
configuration = configuration ? configuration : 'Release';
let privateConfig;
if (privateConfigFile) {
let privateConfigContents = await fs.promise.readFile(privateConfigFile, 'utf8');
privateConfig = JSON.parse(privateConfigContents);
}
let androidBuildConfiguration;
if (keystore && alias && keystorePassword && keyPassword) {
androidBuildConfiguration = {
keystore,
keystorePassword,
keyAlias: alias,
keyPassword,
outputFile,
};
}
let context = StandaloneContext.createServiceContext(
androidSrcPath,
null,
manifest,
privateConfig,
configuration,
url,
releaseChannel,
androidBuildConfiguration
);
await copyInitialShellAppFilesAsync(androidSrcPath, shellPath);
await runShellAppModificationsAsync(context);
if (!args.skipBuild) {
await buildShellAppAsync(context);
}
};
function shellPathForContext(context: StandaloneContext) {
if (context.type === 'user') {
return path.join(context.data.projectPath, 'android');
} else {
return path.join(exponentDirectory(), 'android-shell-app');
}
}
export async function runShellAppModificationsAsync(context: StandaloneContext) {
let shellPath = shellPathForContext(context);
let url: string = context.published.url;
let manifest = context.config; // manifest or app.json
let releaseChannel = context.published.releaseChannel;
if (!context.data.privateConfig) {
console.warn('Warning: No config file specified.');
}
let fullManifestUrl = `${url.replace('exp://', 'https://')}/index.exp`;
let versionCode = 1;
let javaPackage = manifest.android.package;
if (manifest.android.versionCode) {
versionCode = manifest.android.versionCode;
}
if (!javaPackage) {
throw new Error(
'Must specify androidPackage option (either from manifest or on command line).'
);
}
let name = manifest.name;
let iconUrl =
manifest.android && manifest.android.iconUrl ? manifest.android.iconUrl : manifest.iconUrl;
let scheme = manifest.scheme;
let bundleUrl: ?string = manifest.bundleUrl;
let isFullManifest = !!bundleUrl;
let notificationIconUrl = manifest.notification ? manifest.notification.iconUrl : null;
let version = manifest.version ? manifest.version : '0.0.0';
let backgroundImages = backgroundImagesForApp(shellPath, manifest);
let splashBackgroundColor = getSplashScreenBackgroundColor(manifest);
// Clean build directories
await fs.remove(path.join(shellPath, 'app', 'build'));
await fs.remove(path.join(shellPath, 'ReactAndroid', 'build'));
await fs.remove(path.join(shellPath, 'expoview', 'build'));
// Package
await regexFileAsync(
`applicationId 'host.exp.exponent'`,
`applicationId '${javaPackage}'`,
path.join(shellPath, 'app', 'build.gradle')
);
await regexFileAsync(
`android:name="host.exp.exponent"`,
`android:name="${javaPackage}"`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
// Versions
let buildGradleFile = await fs.readFileSync(path.join(shellPath, 'app', 'build.gradle'), 'utf8');
let androidVersion = buildGradleFile.match(/versionName '(\S+)'/)[1];
await regexFileAsync(
'VERSION_NAME = null',
`VERSION_NAME = "${androidVersion}"`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
await deleteLinesInFileAsync(
`BEGIN\ VERSIONS`,
`END\ VERSIONS`,
path.join(shellPath, 'app', 'build.gradle')
);
await regexFileAsync(
'// ADD VERSIONS HERE',
`versionCode ${versionCode}
versionName '${version}'`,
path.join(shellPath, 'app', 'build.gradle')
);
// Remove Exponent build script
await regexFileAsync(
`preBuild.dependsOn generateDynamicMacros`,
``,
path.join(shellPath, 'expoview', 'build.gradle')
);
// change javaMaxHeapSize
await regexFileAsync(
`javaMaxHeapSize "8g"`,
`javaMaxHeapSize "6g"`,
path.join(shellPath, 'app', 'build.gradle')
);
// Push notifications
await regexFileAsync(
'"package_name": "host.exp.exponent"',
`"package_name": "${javaPackage}"`,
path.join(shellPath, 'app', 'google-services.json')
); // TODO: actually use the correct file
// TODO: probably don't need this in both places
await regexFileAsync(
/host\.exp\.exponent\.permission\.C2D_MESSAGE/g,
`${javaPackage}.permission.C2D_MESSAGE`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
await regexFileAsync(
/host\.exp\.exponent\.permission\.C2D_MESSAGE/g,
`${javaPackage}.permission.C2D_MESSAGE`,
path.join(shellPath, 'expoview', 'src', 'main', 'AndroidManifest.xml')
);
// Set INITIAL_URL, SHELL_APP_SCHEME and SHOW_LOADING_VIEW
await regexFileAsync(
'INITIAL_URL = null',
`INITIAL_URL = "${url}"`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
if (scheme) {
await regexFileAsync(
'SHELL_APP_SCHEME = null',
`SHELL_APP_SCHEME = "${scheme}"`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
}
if (shouldShowLoadingView(manifest)) {
await regexFileAsync(
'SHOW_LOADING_VIEW_IN_SHELL_APP = false',
'SHOW_LOADING_VIEW_IN_SHELL_APP = true',
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
}
// App name
await regexFileAsync(
'"app_name">Expo',
`"app_name">${xmlWeirdAndroidEscape(name)}`,
path.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'strings.xml')
);
// Splash Screen background color
await regexFileAsync(
'"splashBackground">#FFFFFF',
`"splashBackground">${splashBackgroundColor}`,
path.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'colors.xml')
);
// show only background color if LoadingView will appear
if (shouldShowLoadingView(manifest)) {
await regexFileAsync(
/<item>.*<\/item>/,
'',
path.join(shellPath, 'app', 'src', 'main', 'res', 'drawable', 'splash_background.xml')
);
}
// Remove exp:// scheme from LauncherActivity
await deleteLinesInFileAsync(
`START\ LAUNCHER\ INTENT\ FILTERS`,
`END\ LAUNCHER\ INTENT\ FILTERS`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
// Remove LAUNCHER category from HomeActivity
await deleteLinesInFileAsync(
`START\ HOME\ INTENT\ FILTERS`,
`END\ HOME\ INTENT\ FILTERS`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
// Add LAUNCHER category to ShellAppActivity
await regexFileAsync(
'<!-- ADD SHELL INTENT FILTERS HERE -->',
`<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
// Add shell app scheme
if (scheme) {
await regexFileAsync(
'<!-- ADD SHELL SCHEME HERE -->',
`<intent-filter>
<data android:scheme="${scheme}"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
}
// Add permissions
if (manifest.android && manifest.android.permissions) {
const content = await fs.readFileSync(
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml'),
'utf-8'
);
// Get the list of optional permissions form manifest
const permissions = content
.replace(
/(([\s\S]*<!-- BEGIN OPTIONAL PERMISSIONS -->)|(<!-- END OPTIONAL PERMISSIONS -->[\s\S]*))/g,
''
)
.match(/android:name=".+"/g)
.map(p => p.replace(/(android:name=|")/g, ''));
const whitelist = [];
manifest.android.permissions.forEach(s => {
if (s.includes('.')) {
whitelist.push(s);
} else {
permissions.forEach(identifier => {
if (identifier.split('.').pop() === s) {
whitelist.push(identifier);
}
});
}
});
// Permissions we need to remove from the generated manifest
const blacklist = [
'android.permission.ACCESS_COARSE_LOCATION',
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.CAMERA',
'android.permission.MANAGE_DOCUMENTS',
'android.permission.READ_CONTACTS',
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.READ_INTERNAL_STORAGE',
'android.permission.READ_PHONE_STATE',
'android.permission.RECORD_AUDIO',
'android.permission.USE_FINGERPRINT',
'android.permission.VIBRATE',
'android.permission.WRITE_EXTERNAL_STORAGE',
'com.anddoes.launcher.permission.UPDATE_COUNT',
'com.android.launcher.permission.INSTALL_SHORTCUT',
'com.google.android.gms.permission.ACTIVITY_RECOGNITION',
'com.google.android.providers.gsf.permission.READ_GSERVICES',
'com.htc.launcher.permission.READ_SETTINGS',
'com.htc.launcher.permission.UPDATE_SHORTCUT',
'com.majeur.launcher.permission.UPDATE_BADGE',
'com.sec.android.provider.badge.permission.READ',
'com.sec.android.provider.badge.permission.WRITE',
'com.sonyericsson.home.permission.BROADCAST_BADGE',
].filter(p => !whitelist.includes(p));
await deleteLinesInFileAsync(
`BEGIN\ OPTIONAL\ PERMISSIONS`,
`END\ OPTIONAL\ PERMISSIONS`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
await regexFileAsync(
'<!-- ADD PERMISSIONS HERE -->',
`
${whitelist.map(p => `<uses-permission android:name="${p}" />`).join('\n')}
${blacklist
.map(p => `<uses-permission android:name="${p}" tools:node="remove" />`)
.join('\n')}
`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
}
// OAuth redirect scheme
await regexFileAsync(
'<data android:scheme="host.exp.exponent" android:path="oauthredirect"/>',
`<data android:scheme="${javaPackage}" android:path="oauthredirect"/>`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
// Embed manifest and bundle
if (isFullManifest) {
await fs.writeFileSync(
path.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app-manifest.json'),
JSON.stringify(manifest)
);
await saveUrlToPathAsync(
bundleUrl,
path.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app.bundle')
);
await regexFileAsync(
'// START EMBEDDED RESPONSES',
`
// START EMBEDDED RESPONSES
embeddedResponses.add(new EmbeddedResponse("${fullManifestUrl}", "assets://shell-app-manifest.json", "application/json"));
embeddedResponses.add(new EmbeddedResponse("${bundleUrl}", "assets://shell-app.bundle", "application/javascript"));`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
}
await regexFileAsync(
'RELEASE_CHANNEL = "default"',
`RELEASE_CHANNEL = "${releaseChannel}"`,
path.join(
shellPath,
'expoview',
'src',
'main',
'java',
'host',
'exp',
'exponent',
'Constants.java'
)
);
// Icon
if (iconUrl) {
(await globby(['**/ic_launcher.png'], {
cwd: path.join(shellPath, 'app', 'src', 'main', 'res'),
absolute: true,
})).forEach(filePath => {
fs.removeSync(filePath);
});
await saveUrlToPathAsync(
iconUrl,
path.join(shellPath, 'app', 'src', 'main', 'res', 'mipmap-hdpi', 'ic_launcher.png')
);
(await globby(['**/ic_launcher.png'], {
cwd: path.join(shellPath, 'expoview', 'src', 'main', 'res'),
absolute: true,
})).forEach(filePath => {
fs.removeSync(filePath);
});
await saveUrlToPathAsync(
iconUrl,
path.join(shellPath, 'expoview', 'src', 'main', 'res', 'mipmap-hdpi', 'ic_launcher.png')
);
}
if (notificationIconUrl) {
(await globby(['**/shell_notification_icon.png'], {
cwd: path.join(shellPath, 'app', 'src', 'main', 'res'),
absolute: true,
})).forEach(filePath => {
fs.removeSync(filePath);
});
await saveUrlToPathAsync(
notificationIconUrl,
path.join(
shellPath,
'app',
'src',
'main',
'res',
'drawable-hdpi',
'shell_notification_icon.png'
)
);
(await globby(['**/shell_notification_icon.png'], {
cwd: path.join(shellPath, 'expoview', 'src', 'main', 'res'),
absolute: true,
})).forEach(filePath => {
fs.removeSync(filePath);
});
await saveUrlToPathAsync(
notificationIconUrl,
path.join(
shellPath,
'expoview',
'src',
'main',
'res',
'drawable-hdpi',
'shell_notification_icon.png'
)
);
}
// Splash Background
if (backgroundImages && backgroundImages.length > 0) {
// Delete the placeholder images
(await globby(['**/shell_launch_background_image.png'], {
cwd: path.join(shellPath, 'expoview', 'src', 'main', 'res'),
absolute: true,
})).forEach(filePath => {
fs.removeSync(filePath);
});
_.forEach(backgroundImages, async image => {
await saveUrlToPathAsync(image.url, image.path);
});
}
let certificateHash = '';
let googleAndroidApiKey = '';
let privateConfig = context.data.privateConfig;
if (privateConfig) {
let branch = privateConfig.branch;
let fabric = privateConfig.fabric;
let googleMaps = privateConfig.googleMaps;
let googleSignIn = privateConfig.googleSignIn;
// Branch
if (branch) {
await regexFileAsync(
'<!-- ADD BRANCH CONFIG HERE -->',
`<meta-data
android:name="io.branch.sdk.BranchKey"
android:value="${branch.apiKey}"/>`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
}
// Fabric
if (fabric) {
await fs.remove(path.join(shellPath, 'app', 'fabric.properties'));
await fs.writeFileSync(
path.join(shellPath, 'app', 'fabric.properties'),
`apiSecret=${fabric.buildSecret}\n`
);
await deleteLinesInFileAsync(
`BEGIN\ FABRIC\ CONFIG`,
`END\ FABRIC\ CONFIG`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
await regexFileAsync(
'<!-- ADD FABRIC CONFIG HERE -->',
`<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabric.apiKey}"/>`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
}
// Google Maps
if (googleMaps) {
await deleteLinesInFileAsync(
`BEGIN\ GOOGLE\ MAPS\ CONFIG`,
`END\ GOOGLE\ MAPS\ CONFIG`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
await regexFileAsync(
'<!-- ADD GOOGLE MAPS CONFIG HERE -->',
`<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${googleMaps.apiKey}"/>`,
path.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')
);
}
// Google Login
if (googleSignIn) {
certificateHash = googleSignIn.certificateHash;
googleAndroidApiKey = googleSignIn.apiKey;
}
}
// Google sign in
await regexFileAsync(
/"current_key": "(.*?)"/,
`"current_key": "${googleAndroidApiKey}"`,
path.join(shellPath, 'app', 'google-services.json')
);
await regexFileAsync(
/"certificate_hash": "(.*?)"/,
`"certificate_hash": "${certificateHash}"`,
path.join(shellPath, 'app', 'google-services.json')
);
}
async function buildShellAppAsync(context: StandaloneContext) {
let shellPath = shellPathForContext(context);
if (context.build.android) {
let androidBuildConfiguration = context.build.android;
try {
await fs.remove(`shell-unaligned.apk`);
await fs.remove(`shell.apk`);
} catch (e) {}
const gradleArgs = [`assembleProdRelease`];
if (!!process.env.GRADLE_DAEMON_DISABLED) {
gradleArgs.unshift('--no-daemon');
}
await spawnAsyncThrowError(`./gradlew`, gradleArgs, {
stdio: 'inherit',
cwd: shellPath,
});
await fs.copy(
path.join(shellPath, 'app', 'build', 'outputs', 'apk', 'app-prod-release-unsigned.apk'),
`shell-unaligned.apk`
);
await spawnAsync(`jarsigner`, [
'-verbose',
'-sigalg',
'SHA1withRSA',
'-digestalg',
'SHA1',
'-storepass',
androidBuildConfiguration.keystorePassword,
'-keypass',
androidBuildConfiguration.keyPassword,
'-keystore',
androidBuildConfiguration.keystore,
'shell-unaligned.apk',
androidBuildConfiguration.keyAlias,
]);
await spawnAsync(`zipalign`, ['-v', '4', 'shell-unaligned.apk', 'shell.apk']);
try {
await fs.remove('shell-unaligned.apk');
} catch (e) {}
await spawnAsync(`jarsigner`, [
'-verify',
'-verbose',
'-certs',
'-keystore',
androidBuildConfiguration.keystore,
'shell.apk',
]);
await fs.copy('shell.apk', androidBuildConfiguration.outputFile || '/tmp/shell-signed.apk');
} else {
let androidSrcPath = path.join(exponentDirectory(), 'android');
await fs.copy(
path.join(androidSrcPath, 'debug.keystore'),
path.join(shellPath, 'debug.keystore')
);
try {
await fs.remove('shell-debug.apk');
} catch (e) {}
await spawnAsyncThrowError(`./gradlew`, ['assembleDevRemoteKernelDebug'], {
stdio: 'inherit',
cwd: shellPath,
});
await fs.copy(
path.join(shellPath, 'app', 'build', 'outputs', 'apk', 'app-devRemoteKernel-debug.apk'),
`/tmp/shell-debug.apk`
);
}
}