Skip to content

Commit 6d4e620

Browse files
authored
feat(cli): add build command for android (#5891)
* 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
1 parent 3d4433b commit 6d4e620

File tree

6 files changed

+263
-1
lines changed

6 files changed

+263
-1
lines changed

cli/src/android/build.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { join } from 'path';
2+
3+
import c from '../colors';
4+
import { runTask } from '../common';
5+
import type { Config } from '../definitions';
6+
import { logSuccess } from '../log';
7+
import type { BuildCommandOptions } from '../tasks/build';
8+
import { runCommand } from '../util/subprocess';
9+
10+
export async function buildAndroid(
11+
config: Config,
12+
buildOptions: BuildCommandOptions,
13+
): Promise<void> {
14+
const releaseType = buildOptions.androidreleasetype ?? 'AAB';
15+
const releaseTypeIsAAB = releaseType === 'AAB';
16+
const arg = releaseTypeIsAAB ? ':app:bundleRelease' : 'assembleRelease';
17+
const gradleArgs = [arg];
18+
19+
if (
20+
!buildOptions.keystorepath ||
21+
!buildOptions.keystorealias ||
22+
!buildOptions.keystorealiaspass ||
23+
!buildOptions.keystorepass
24+
) {
25+
throw 'Missing options. Please supply all options for android signing. (Keystore Path, Keystore Password, Keystore Key Alias, Keystore Key Password)';
26+
}
27+
28+
try {
29+
await runTask('Running Gradle build', async () =>
30+
runCommand('./gradlew', gradleArgs, {
31+
cwd: config.android.platformDirAbs,
32+
}),
33+
);
34+
} catch (e) {
35+
if ((e as any).includes('EACCES')) {
36+
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(
37+
`chmod +x ./${config.android.platformDir}/gradlew`,
38+
)} and try again.`;
39+
} else {
40+
throw e;
41+
}
42+
}
43+
44+
const releasePath = join(
45+
config.android.appDirAbs,
46+
'build',
47+
'outputs',
48+
releaseTypeIsAAB ? 'bundle' : 'apk',
49+
'release',
50+
);
51+
52+
const unsignedReleaseName = `app${
53+
config.android.flavor ? `-${config.android.flavor}` : ''
54+
}-release${releaseTypeIsAAB ? '' : '-unsigned'}.${releaseType.toLowerCase()}`;
55+
56+
const signedReleaseName = unsignedReleaseName.replace(
57+
`-release${
58+
releaseTypeIsAAB ? '' : '-unsigned'
59+
}.${releaseType.toLowerCase()}`,
60+
`-release-signed.${releaseType.toLowerCase()}`,
61+
);
62+
63+
const signingArgs = [
64+
'-sigalg',
65+
'SHA1withRSA',
66+
'-digestalg',
67+
'SHA1',
68+
'-keystore',
69+
buildOptions.keystorepath,
70+
'-keypass',
71+
buildOptions.keystorealiaspass,
72+
'-storepass',
73+
buildOptions.keystorepass,
74+
`-signedjar`,
75+
`${join(releasePath, signedReleaseName)}`,
76+
`${join(releasePath, unsignedReleaseName)}`,
77+
buildOptions.keystorealias,
78+
];
79+
80+
await runTask('Signing Release', async () => {
81+
await runCommand('jarsigner', signingArgs, {
82+
cwd: config.android.platformDirAbs,
83+
});
84+
});
85+
86+
logSuccess(`Successfully generated ${signedReleaseName} at: ${releasePath}`);
87+
}

cli/src/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@ async function loadAndroidConfig(
236236
const buildOutputDir = `${apkPath}/debug`;
237237
const cordovaPluginsDir = 'capacitor-cordova-android-plugins';
238238
const studioPath = lazy(() => determineAndroidStudioPath(cliConfig.os));
239+
const buildOptions = {
240+
keystorePath: extConfig.android?.buildOptions?.keystorePath,
241+
keystorePassword: extConfig.android?.buildOptions?.keystorePassword,
242+
keystoreAlias: extConfig.android?.buildOptions?.keystoreAlias,
243+
keystoreAliasPassword:
244+
extConfig.android?.buildOptions?.keystoreAliasPassword,
245+
releaseType: extConfig.android?.buildOptions?.releaseType,
246+
};
239247

240248
return {
241249
name,
@@ -261,6 +269,7 @@ async function loadAndroidConfig(
261269
buildOutputDir,
262270
buildOutputDirAbs: resolve(platformDirAbs, buildOutputDir),
263271
flavor,
272+
buildOptions,
264273
};
265274
}
266275

cli/src/declarations.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,44 @@ export interface CapacitorConfig {
225225
* @default 60
226226
*/
227227
minWebViewVersion?: number;
228+
229+
buildOptions?: {
230+
/**
231+
* Path to your keystore
232+
*
233+
* @since 4.3.0
234+
*/
235+
keystorePath?: string;
236+
237+
/**
238+
* Password to your keystore
239+
*
240+
* @since 4.3.0
241+
*/
242+
keystorePassword?: string;
243+
244+
/**
245+
* Alias in the keystore to use
246+
*
247+
* @since 4.3.0
248+
*/
249+
keystoreAlias?: string;
250+
251+
/**
252+
* Password for the alias in the keystore to use
253+
*
254+
* @since 4.3.0
255+
*/
256+
keystoreAliasPassword?: string;
257+
258+
/**
259+
* Bundle type for your release build
260+
*
261+
* @since 4.3.0
262+
* @default "AAB"
263+
*/
264+
releaseType?: 'AAB' | 'APK';
265+
};
228266
};
229267

230268
ios?: {

cli/src/definitions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ export interface AndroidConfig extends PlatformConfig {
9191
readonly buildOutputDirAbs: string;
9292
readonly apkName: string;
9393
readonly flavor: string;
94+
readonly buildOptions: {
95+
keystorePath?: string;
96+
keystorePassword?: string;
97+
keystoreAlias?: string;
98+
keystoreAliasPassword?: string;
99+
releaseType?: 'AAB' | 'APK';
100+
};
94101
}
95102

96103
export interface IOSConfig extends PlatformConfig {

cli/src/index.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { program } from 'commander';
1+
import { Option, program } from 'commander';
22

33
import c from './colors';
44
import { checkExternalConfig, loadConfig } from './config';
@@ -137,6 +137,51 @@ export function runProgram(config: Config): void {
137137
),
138138
);
139139

140+
program
141+
.command('build <platform>')
142+
.description('builds the release version of the selected platform')
143+
.option('--keystorepath <keystorePath>', 'Path to the keystore')
144+
.option('--keystorepass <keystorePass>', 'Password to the keystore')
145+
.option('--keystorealias <keystoreAlias>', 'Key Alias in the keystore')
146+
.option(
147+
'--keystorealiaspass <keystoreAliasPass>',
148+
'Password for the Key Alias',
149+
)
150+
.addOption(
151+
new Option(
152+
'--androidreleasetype <androidreleasetype>',
153+
'Android release type; APK or AAB',
154+
)
155+
.choices(['AAB', 'APK'])
156+
.default('AAB'),
157+
)
158+
.action(
159+
wrapAction(
160+
telemetryAction(
161+
config,
162+
async (
163+
platform,
164+
{
165+
keystorepath,
166+
keystorepass,
167+
keystorealias,
168+
keystorealiaspass,
169+
androidreleasetype,
170+
},
171+
) => {
172+
const { buildCommand } = await import('./tasks/build');
173+
await buildCommand(config, platform, {
174+
keystorepath,
175+
keystorepass,
176+
keystorealias,
177+
keystorealiaspass,
178+
androidreleasetype,
179+
});
180+
},
181+
),
182+
),
183+
);
184+
140185
program
141186
.command(`run [platform]`)
142187
.description(

cli/src/tasks/build.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { buildAndroid } from '../android/build';
2+
import { selectPlatforms, promptForPlatform } from '../common';
3+
import type { Config } from '../definitions';
4+
import { fatal, isFatal } from '../errors';
5+
6+
export interface BuildCommandOptions {
7+
keystorepath?: string;
8+
keystorepass?: string;
9+
keystorealias?: string;
10+
keystorealiaspass?: string;
11+
androidreleasetype?: 'AAB' | 'APK';
12+
}
13+
14+
export async function buildCommand(
15+
config: Config,
16+
selectedPlatformName: string,
17+
buildOptions: BuildCommandOptions,
18+
): Promise<void> {
19+
const platforms = await selectPlatforms(config, selectedPlatformName);
20+
let platformName: string;
21+
if (platforms.length === 1) {
22+
platformName = platforms[0];
23+
} else {
24+
platformName = await promptForPlatform(
25+
platforms.filter(createBuildablePlatformFilter(config)),
26+
`Please choose a platform to build for:`,
27+
);
28+
}
29+
30+
const buildCommandOptions: BuildCommandOptions = {
31+
keystorepath:
32+
buildOptions.keystorepath || config.android.buildOptions.keystorePath,
33+
keystorepass:
34+
buildOptions.keystorepass || config.android.buildOptions.keystorePassword,
35+
keystorealias:
36+
buildOptions.keystorealias || config.android.buildOptions.keystoreAlias,
37+
keystorealiaspass:
38+
buildOptions.keystorealiaspass ||
39+
config.android.buildOptions.keystoreAliasPassword,
40+
androidreleasetype:
41+
buildOptions.androidreleasetype ||
42+
config.android.buildOptions.releaseType,
43+
};
44+
45+
try {
46+
await build(config, platformName, buildCommandOptions);
47+
} catch (e) {
48+
if (!isFatal(e)) {
49+
fatal((e as any).stack ?? e);
50+
}
51+
throw e;
52+
}
53+
}
54+
55+
export async function build(
56+
config: Config,
57+
platformName: string,
58+
buildOptions: BuildCommandOptions,
59+
): Promise<void> {
60+
if (platformName == config.ios.name) {
61+
throw `Platform "${platformName}" is not available in the build command.`;
62+
} else if (platformName === config.android.name) {
63+
await buildAndroid(config, buildOptions);
64+
} else if (platformName === config.web.name) {
65+
throw `Platform "${platformName}" is not available in the build command.`;
66+
} else {
67+
throw `Platform "${platformName}" is not valid.`;
68+
}
69+
}
70+
71+
function createBuildablePlatformFilter(
72+
config: Config,
73+
): (platform: string) => boolean {
74+
return platform =>
75+
platform === config.ios.name || platform === config.android.name;
76+
}

0 commit comments

Comments
 (0)