Skip to content

Commit

Permalink
feat(cli): add build command for android (#5891)
Browse files Browse the repository at this point in the history
* feat(cli): add build command for android

* chore: remove console log

* chore: mesage if try ios

* chore: remove unused

* chore: add cap-config file support
  • Loading branch information
IT-MikeS authored Sep 28, 2022
1 parent 3d4433b commit 6d4e620
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 1 deletion.
87 changes: 87 additions & 0 deletions cli/src/android/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { join } from 'path';

import c from '../colors';
import { runTask } from '../common';
import type { Config } from '../definitions';
import { logSuccess } from '../log';
import type { BuildCommandOptions } from '../tasks/build';
import { runCommand } from '../util/subprocess';

export async function buildAndroid(
config: Config,
buildOptions: BuildCommandOptions,
): Promise<void> {
const releaseType = buildOptions.androidreleasetype ?? 'AAB';
const releaseTypeIsAAB = releaseType === 'AAB';
const arg = releaseTypeIsAAB ? ':app:bundleRelease' : 'assembleRelease';
const gradleArgs = [arg];

if (
!buildOptions.keystorepath ||
!buildOptions.keystorealias ||
!buildOptions.keystorealiaspass ||
!buildOptions.keystorepass
) {
throw 'Missing options. Please supply all options for android signing. (Keystore Path, Keystore Password, Keystore Key Alias, Keystore Key Password)';
}

try {
await runTask('Running Gradle build', async () =>
runCommand('./gradlew', gradleArgs, {
cwd: config.android.platformDirAbs,
}),
);
} catch (e) {
if ((e as any).includes('EACCES')) {
throw `gradlew file does not have executable permissions. This can happen if the Android platform was added on a Windows machine. Please run ${c.strong(
`chmod +x ./${config.android.platformDir}/gradlew`,
)} and try again.`;
} else {
throw e;
}
}

const releasePath = join(
config.android.appDirAbs,
'build',
'outputs',
releaseTypeIsAAB ? 'bundle' : 'apk',
'release',
);

const unsignedReleaseName = `app${
config.android.flavor ? `-${config.android.flavor}` : ''
}-release${releaseTypeIsAAB ? '' : '-unsigned'}.${releaseType.toLowerCase()}`;

const signedReleaseName = unsignedReleaseName.replace(
`-release${
releaseTypeIsAAB ? '' : '-unsigned'
}.${releaseType.toLowerCase()}`,
`-release-signed.${releaseType.toLowerCase()}`,
);

const signingArgs = [
'-sigalg',
'SHA1withRSA',
'-digestalg',
'SHA1',
'-keystore',
buildOptions.keystorepath,
'-keypass',
buildOptions.keystorealiaspass,
'-storepass',
buildOptions.keystorepass,
`-signedjar`,
`${join(releasePath, signedReleaseName)}`,
`${join(releasePath, unsignedReleaseName)}`,
buildOptions.keystorealias,
];

await runTask('Signing Release', async () => {
await runCommand('jarsigner', signingArgs, {
cwd: config.android.platformDirAbs,
});
});

logSuccess(`Successfully generated ${signedReleaseName} at: ${releasePath}`);
}
9 changes: 9 additions & 0 deletions cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ async function loadAndroidConfig(
const buildOutputDir = `${apkPath}/debug`;
const cordovaPluginsDir = 'capacitor-cordova-android-plugins';
const studioPath = lazy(() => determineAndroidStudioPath(cliConfig.os));
const buildOptions = {
keystorePath: extConfig.android?.buildOptions?.keystorePath,
keystorePassword: extConfig.android?.buildOptions?.keystorePassword,
keystoreAlias: extConfig.android?.buildOptions?.keystoreAlias,
keystoreAliasPassword:
extConfig.android?.buildOptions?.keystoreAliasPassword,
releaseType: extConfig.android?.buildOptions?.releaseType,
};

return {
name,
Expand All @@ -261,6 +269,7 @@ async function loadAndroidConfig(
buildOutputDir,
buildOutputDirAbs: resolve(platformDirAbs, buildOutputDir),
flavor,
buildOptions,
};
}

Expand Down
38 changes: 38 additions & 0 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,44 @@ export interface CapacitorConfig {
* @default 60
*/
minWebViewVersion?: number;

buildOptions?: {
/**
* Path to your keystore
*
* @since 4.3.0
*/
keystorePath?: string;

/**
* Password to your keystore
*
* @since 4.3.0
*/
keystorePassword?: string;

/**
* Alias in the keystore to use
*
* @since 4.3.0
*/
keystoreAlias?: string;

/**
* Password for the alias in the keystore to use
*
* @since 4.3.0
*/
keystoreAliasPassword?: string;

/**
* Bundle type for your release build
*
* @since 4.3.0
* @default "AAB"
*/
releaseType?: 'AAB' | 'APK';
};
};

ios?: {
Expand Down
7 changes: 7 additions & 0 deletions cli/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export interface AndroidConfig extends PlatformConfig {
readonly buildOutputDirAbs: string;
readonly apkName: string;
readonly flavor: string;
readonly buildOptions: {
keystorePath?: string;
keystorePassword?: string;
keystoreAlias?: string;
keystoreAliasPassword?: string;
releaseType?: 'AAB' | 'APK';
};
}

export interface IOSConfig extends PlatformConfig {
Expand Down
47 changes: 46 additions & 1 deletion cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { program } from 'commander';
import { Option, program } from 'commander';

import c from './colors';
import { checkExternalConfig, loadConfig } from './config';
Expand Down Expand Up @@ -137,6 +137,51 @@ export function runProgram(config: Config): void {
),
);

program
.command('build <platform>')
.description('builds the release version of the selected platform')
.option('--keystorepath <keystorePath>', 'Path to the keystore')
.option('--keystorepass <keystorePass>', 'Password to the keystore')
.option('--keystorealias <keystoreAlias>', 'Key Alias in the keystore')
.option(
'--keystorealiaspass <keystoreAliasPass>',
'Password for the Key Alias',
)
.addOption(
new Option(
'--androidreleasetype <androidreleasetype>',
'Android release type; APK or AAB',
)
.choices(['AAB', 'APK'])
.default('AAB'),
)
.action(
wrapAction(
telemetryAction(
config,
async (
platform,
{
keystorepath,
keystorepass,
keystorealias,
keystorealiaspass,
androidreleasetype,
},
) => {
const { buildCommand } = await import('./tasks/build');
await buildCommand(config, platform, {
keystorepath,
keystorepass,
keystorealias,
keystorealiaspass,
androidreleasetype,
});
},
),
),
);

program
.command(`run [platform]`)
.description(
Expand Down
76 changes: 76 additions & 0 deletions cli/src/tasks/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { buildAndroid } from '../android/build';
import { selectPlatforms, promptForPlatform } from '../common';
import type { Config } from '../definitions';
import { fatal, isFatal } from '../errors';

export interface BuildCommandOptions {
keystorepath?: string;
keystorepass?: string;
keystorealias?: string;
keystorealiaspass?: string;
androidreleasetype?: 'AAB' | 'APK';
}

export async function buildCommand(
config: Config,
selectedPlatformName: string,
buildOptions: BuildCommandOptions,
): Promise<void> {
const platforms = await selectPlatforms(config, selectedPlatformName);
let platformName: string;
if (platforms.length === 1) {
platformName = platforms[0];
} else {
platformName = await promptForPlatform(
platforms.filter(createBuildablePlatformFilter(config)),
`Please choose a platform to build for:`,
);
}

const buildCommandOptions: BuildCommandOptions = {
keystorepath:
buildOptions.keystorepath || config.android.buildOptions.keystorePath,
keystorepass:
buildOptions.keystorepass || config.android.buildOptions.keystorePassword,
keystorealias:
buildOptions.keystorealias || config.android.buildOptions.keystoreAlias,
keystorealiaspass:
buildOptions.keystorealiaspass ||
config.android.buildOptions.keystoreAliasPassword,
androidreleasetype:
buildOptions.androidreleasetype ||
config.android.buildOptions.releaseType,
};

try {
await build(config, platformName, buildCommandOptions);
} catch (e) {
if (!isFatal(e)) {
fatal((e as any).stack ?? e);
}
throw e;
}
}

export async function build(
config: Config,
platformName: string,
buildOptions: BuildCommandOptions,
): Promise<void> {
if (platformName == config.ios.name) {
throw `Platform "${platformName}" is not available in the build command.`;
} else if (platformName === config.android.name) {
await buildAndroid(config, buildOptions);
} else if (platformName === config.web.name) {
throw `Platform "${platformName}" is not available in the build command.`;
} else {
throw `Platform "${platformName}" is not valid.`;
}
}

function createBuildablePlatformFilter(
config: Config,
): (platform: string) => boolean {
return platform =>
platform === config.ios.name || platform === config.android.name;
}

0 comments on commit 6d4e620

Please sign in to comment.