From 75f4dc0a3e38de3b711661f94c6ac82b516eac92 Mon Sep 17 00:00:00 2001 From: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com> Date: Tue, 30 Aug 2022 09:56:31 -0400 Subject: [PATCH 1/7] feat(cli): add build command for android --- cli/src/android/build.ts | 88 ++++++++++++++++++++++++++++++++++++++++ cli/src/index.ts | 48 +++++++++++++++++++++- cli/src/tasks/build.ts | 62 ++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 cli/src/android/build.ts create mode 100644 cli/src/tasks/build.ts diff --git a/cli/src/android/build.ts b/cli/src/android/build.ts new file mode 100644 index 000000000..1b0eeae45 --- /dev/null +++ b/cli/src/android/build.ts @@ -0,0 +1,88 @@ +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 { + const releaseTypeIsAAB = buildOptions.androidreleasetype === '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 aab 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' + }.${buildOptions.androidreleasetype.toLowerCase()}`; + + const signedReleaseName = unsignedReleaseName.replace( + `-release${ + releaseTypeIsAAB ? '' : '-unsigned' + }.${buildOptions.androidreleasetype.toLowerCase()}`, + `-release-signed.${buildOptions.androidreleasetype.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}`); +} diff --git a/cli/src/index.ts b/cli/src/index.ts index f3c4eaf87..7806020ff 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,4 +1,4 @@ -import { program } from 'commander'; +import { Option, program } from 'commander'; import c from './colors'; import { checkExternalConfig, loadConfig } from './config'; @@ -132,6 +132,52 @@ export function runProgram(config: Config): void { ), ); + program + .command('build ') + .description('builds the release version of the selected platform') + .option('--keystorepath ', 'Path to the keystore') + .option('--keystorepass ', 'Password to the keystore') + .option('--keystorealias ', 'Key Alias in the keystore') + .option( + '--keystorealiaspass ', + 'Password for the Key Alias', + ) + .addOption( + new Option( + '--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'); + console.log(program.args); + await buildCommand(config, platform, { + keystorepath, + keystorepass, + keystorealias, + keystorealiaspass, + androidreleasetype, + }); + }, + ), + ), + ); + program .command(`run [platform]`) .description( diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts new file mode 100644 index 000000000..d8cbec6ad --- /dev/null +++ b/cli/src/tasks/build.ts @@ -0,0 +1,62 @@ +import { buildAndroid } from '../android/build'; +import c from '../colors'; +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: string; +} + +export async function buildCommand( + config: Config, + selectedPlatformName: string, + buildOptions: BuildCommandOptions, +): Promise { + 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:`, + ); + } + + try { + await build(config, platformName, buildOptions); + } catch (e) { + if (!isFatal(e)) { + fatal((e as any).stack ?? e); + } + throw e; + } +} + +export async function build( + config: Config, + platformName: string, + buildOptions: BuildCommandOptions, +): Promise { + if (platformName == config.ios.name) { + // await runIOS(config); + } 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; +} From 1bc76fb94ce681905221a7090cb082a5998ab6de Mon Sep 17 00:00:00 2001 From: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com> Date: Tue, 30 Aug 2022 10:01:32 -0400 Subject: [PATCH 2/7] chore: remove console log --- cli/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/index.ts b/cli/src/index.ts index 7806020ff..a1d84486b 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -165,7 +165,6 @@ export function runProgram(config: Config): void { }, ) => { const { buildCommand } = await import('./tasks/build'); - console.log(program.args); await buildCommand(config, platform, { keystorepath, keystorepass, From a35ce6befc3974847a423dc4a5d1487a9be67484 Mon Sep 17 00:00:00 2001 From: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com> Date: Tue, 30 Aug 2022 10:30:56 -0400 Subject: [PATCH 3/7] chore: mesage if try ios --- cli/src/tasks/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts index d8cbec6ad..7b954dd45 100644 --- a/cli/src/tasks/build.ts +++ b/cli/src/tasks/build.ts @@ -44,7 +44,7 @@ export async function build( buildOptions: BuildCommandOptions, ): Promise { if (platformName == config.ios.name) { - // await runIOS(config); + 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) { From 9484d4c237515c0fcfae2b51b080549264fe24df Mon Sep 17 00:00:00 2001 From: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com> Date: Tue, 30 Aug 2022 11:26:07 -0400 Subject: [PATCH 4/7] chore: remove unused --- cli/src/tasks/build.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts index 7b954dd45..d0815b000 100644 --- a/cli/src/tasks/build.ts +++ b/cli/src/tasks/build.ts @@ -1,5 +1,4 @@ import { buildAndroid } from '../android/build'; -import c from '../colors'; import { selectPlatforms, promptForPlatform } from '../common'; import type { Config } from '../definitions'; import { fatal, isFatal } from '../errors'; From 0b24259ce48e83c4f49227dffbd3121869686ee9 Mon Sep 17 00:00:00 2001 From: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com> Date: Tue, 13 Sep 2022 16:09:56 -0400 Subject: [PATCH 5/7] chore: add cap-config file support --- cli/src/android/build.ts | 13 ++++++------- cli/src/config.ts | 9 +++++++++ cli/src/declarations.ts | 38 ++++++++++++++++++++++++++++++++++++++ cli/src/definitions.ts | 7 +++++++ cli/src/tasks/build.ts | 27 +++++++++++++++++++++------ 5 files changed, 81 insertions(+), 13 deletions(-) diff --git a/cli/src/android/build.ts b/cli/src/android/build.ts index 1b0eeae45..de22a2ded 100644 --- a/cli/src/android/build.ts +++ b/cli/src/android/build.ts @@ -11,7 +11,8 @@ export async function buildAndroid( config: Config, buildOptions: BuildCommandOptions, ): Promise { - const releaseTypeIsAAB = buildOptions.androidreleasetype === 'AAB'; + const releaseType = buildOptions.androidreleasetype ?? 'AAB'; + const releaseTypeIsAAB = releaseType === 'AAB'; const arg = releaseTypeIsAAB ? ':app:bundleRelease' : 'assembleRelease'; const gradleArgs = [arg]; @@ -21,7 +22,7 @@ export async function buildAndroid( !buildOptions.keystorealiaspass || !buildOptions.keystorepass ) { - throw 'Missing options. Please supply all options for android aab signing. (Keystore Path, Keystore Password, Keystore Key Alias, Keystore Key Password)'; + throw 'Missing options. Please supply all options for android signing. (Keystore Path, Keystore Password, Keystore Key Alias, Keystore Key Password)'; } try { @@ -50,15 +51,13 @@ export async function buildAndroid( const unsignedReleaseName = `app${ config.android.flavor ? `-${config.android.flavor}` : '' - }-release${ - releaseTypeIsAAB ? '' : '-unsigned' - }.${buildOptions.androidreleasetype.toLowerCase()}`; + }-release${releaseTypeIsAAB ? '' : '-unsigned'}.${releaseType.toLowerCase()}`; const signedReleaseName = unsignedReleaseName.replace( `-release${ releaseTypeIsAAB ? '' : '-unsigned' - }.${buildOptions.androidreleasetype.toLowerCase()}`, - `-release-signed.${buildOptions.androidreleasetype.toLowerCase()}`, + }.${releaseType.toLowerCase()}`, + `-release-signed.${releaseType.toLowerCase()}`, ); const signingArgs = [ diff --git a/cli/src/config.ts b/cli/src/config.ts index c6f75c34f..903b3619b 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -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, @@ -261,6 +269,7 @@ async function loadAndroidConfig( buildOutputDir, buildOutputDirAbs: resolve(platformDirAbs, buildOutputDir), flavor, + buildOptions, }; } diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 63b69d1d4..1e4432baf 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -221,6 +221,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?: { diff --git a/cli/src/definitions.ts b/cli/src/definitions.ts index 445d5312b..a2e0b6d24 100644 --- a/cli/src/definitions.ts +++ b/cli/src/definitions.ts @@ -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 { diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts index d0815b000..e57e54cf8 100644 --- a/cli/src/tasks/build.ts +++ b/cli/src/tasks/build.ts @@ -4,11 +4,11 @@ import type { Config } from '../definitions'; import { fatal, isFatal } from '../errors'; export interface BuildCommandOptions { - keystorepath: string; - keystorepass: string; - keystorealias: string; - keystorealiaspass: string; - androidreleasetype: string; + keystorepath?: string; + keystorepass?: string; + keystorealias?: string; + keystorealiaspass?: string; + androidreleasetype?: 'AAB' | 'APK'; } export async function buildCommand( @@ -27,8 +27,23 @@ export async function buildCommand( ); } + 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, buildOptions); + await build(config, platformName, buildCommandOptions); } catch (e) { if (!isFatal(e)) { fatal((e as any).stack ?? e); From 8cf16154a87ea11656588e5d5776d90d0b17c8c3 Mon Sep 17 00:00:00 2001 From: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com> Date: Wed, 14 Sep 2022 11:34:22 -0400 Subject: [PATCH 6/7] feat(cli): add build command for iOS IPA --- cli/src/index.ts | 3 ++ cli/src/ios/build.ts | 68 ++++++++++++++++++++++++++++++++++++++++++ cli/src/tasks/build.ts | 6 +++- 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 cli/src/ios/build.ts diff --git a/cli/src/index.ts b/cli/src/index.ts index a1d84486b..2a9dc316b 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -135,6 +135,7 @@ export function runProgram(config: Config): void { program .command('build ') .description('builds the release version of the selected platform') + .option('--scheme ', 'iOS Scheme to build') .option('--keystorepath ', 'Path to the keystore') .option('--keystorepass ', 'Password to the keystore') .option('--keystorealias ', 'Key Alias in the keystore') @@ -157,6 +158,7 @@ export function runProgram(config: Config): void { async ( platform, { + scheme, keystorepath, keystorepass, keystorealias, @@ -166,6 +168,7 @@ export function runProgram(config: Config): void { ) => { const { buildCommand } = await import('./tasks/build'); await buildCommand(config, platform, { + scheme, keystorepath, keystorepass, keystorealias, diff --git a/cli/src/ios/build.ts b/cli/src/ios/build.ts new file mode 100644 index 000000000..38e41d1af --- /dev/null +++ b/cli/src/ios/build.ts @@ -0,0 +1,68 @@ +import { writeFileSync, unlinkSync } from '@ionic/utils-fs'; +import { join } from 'path'; +import rimraf from 'rimraf'; + +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 buildiOS( + config: Config, + buildOptions: BuildCommandOptions, +): Promise { + const theScheme = buildOptions.scheme ?? 'App'; + + await runTask('Building xArchive', async () => + runCommand('xcodebuild', [ + '-workspace', + `${theScheme}.xcworkspace`, + '-scheme', + `${theScheme}`, + '-destination', + `generic/platform=iOS`, + '-archivePath', + `${theScheme}.xcarchive`, + 'archive' + ], { + cwd: config.ios.nativeProjectDirAbs, + }), + ); + + const archivePlistContents = ` + + + +method +app-store + +`; + + const archivePlistPath = join(`${config.ios.nativeProjectDirAbs}`, 'archive.plist'); + + writeFileSync(archivePlistPath, archivePlistContents); + + await runTask('Building IPA', async () => + runCommand('xcodebuild', [ + 'archive', + '-archivePath', + `${theScheme}.xcarchive`, + '-exportArchive', + '-exportOptionsPlist', + 'archive.plist', + '-exportPath', + 'output', + '-allowProvisioningUpdates' + ], { + cwd: config.ios.nativeProjectDirAbs, + }), + ); + + await runTask('Cleaning up', async () => { + unlinkSync(archivePlistPath); + rimraf.sync(join(config.ios.nativeProjectDirAbs, `${theScheme}.xcarchive`)); + }); + + logSuccess(`Successfully generated an IPA at: ${join(config.ios.nativeProjectDirAbs, 'output')}`); +} diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts index e57e54cf8..c80d6cf11 100644 --- a/cli/src/tasks/build.ts +++ b/cli/src/tasks/build.ts @@ -2,8 +2,10 @@ import { buildAndroid } from '../android/build'; import { selectPlatforms, promptForPlatform } from '../common'; import type { Config } from '../definitions'; import { fatal, isFatal } from '../errors'; +import { buildiOS } from '../ios/build'; export interface BuildCommandOptions { + scheme?: string; keystorepath?: string; keystorepass?: string; keystorealias?: string; @@ -28,6 +30,8 @@ export async function buildCommand( } const buildCommandOptions: BuildCommandOptions = { + scheme: + buildOptions.scheme || config.ios.scheme, keystorepath: buildOptions.keystorepath || config.android.buildOptions.keystorePath, keystorepass: @@ -58,7 +62,7 @@ export async function build( buildOptions: BuildCommandOptions, ): Promise { if (platformName == config.ios.name) { - throw `Platform "${platformName}" is not available in the build command.`; + await buildiOS(config, buildOptions); } else if (platformName === config.android.name) { await buildAndroid(config, buildOptions); } else if (platformName === config.web.name) { From e1246c14f35feabd57ab9a54604d4cd4baff9be0 Mon Sep 17 00:00:00 2001 From: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com> Date: Thu, 15 Sep 2022 12:15:32 -0400 Subject: [PATCH 7/7] chore: fmt --- cli/src/ios/build.ts | 74 +++++++++++++++++++++++++----------------- cli/src/tasks/build.ts | 3 +- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/cli/src/ios/build.ts b/cli/src/ios/build.ts index 38e41d1af..c8f8b31ab 100644 --- a/cli/src/ios/build.ts +++ b/cli/src/ios/build.ts @@ -15,19 +15,23 @@ export async function buildiOS( const theScheme = buildOptions.scheme ?? 'App'; await runTask('Building xArchive', async () => - runCommand('xcodebuild', [ - '-workspace', - `${theScheme}.xcworkspace`, - '-scheme', - `${theScheme}`, - '-destination', - `generic/platform=iOS`, - '-archivePath', - `${theScheme}.xcarchive`, - 'archive' - ], { - cwd: config.ios.nativeProjectDirAbs, - }), + runCommand( + 'xcodebuild', + [ + '-workspace', + `${theScheme}.xcworkspace`, + '-scheme', + `${theScheme}`, + '-destination', + `generic/platform=iOS`, + '-archivePath', + `${theScheme}.xcarchive`, + 'archive', + ], + { + cwd: config.ios.nativeProjectDirAbs, + }, + ), ); const archivePlistContents = ` @@ -39,30 +43,42 @@ export async function buildiOS( `; - const archivePlistPath = join(`${config.ios.nativeProjectDirAbs}`, 'archive.plist'); + const archivePlistPath = join( + `${config.ios.nativeProjectDirAbs}`, + 'archive.plist', + ); writeFileSync(archivePlistPath, archivePlistContents); await runTask('Building IPA', async () => - runCommand('xcodebuild', [ - 'archive', - '-archivePath', - `${theScheme}.xcarchive`, - '-exportArchive', - '-exportOptionsPlist', - 'archive.plist', - '-exportPath', - 'output', - '-allowProvisioningUpdates' - ], { - cwd: config.ios.nativeProjectDirAbs, - }), + runCommand( + 'xcodebuild', + [ + 'archive', + '-archivePath', + `${theScheme}.xcarchive`, + '-exportArchive', + '-exportOptionsPlist', + 'archive.plist', + '-exportPath', + 'output', + '-allowProvisioningUpdates', + ], + { + cwd: config.ios.nativeProjectDirAbs, + }, + ), ); - + await runTask('Cleaning up', async () => { unlinkSync(archivePlistPath); rimraf.sync(join(config.ios.nativeProjectDirAbs, `${theScheme}.xcarchive`)); }); - logSuccess(`Successfully generated an IPA at: ${join(config.ios.nativeProjectDirAbs, 'output')}`); + logSuccess( + `Successfully generated an IPA at: ${join( + config.ios.nativeProjectDirAbs, + 'output', + )}`, + ); } diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts index c80d6cf11..5602f75da 100644 --- a/cli/src/tasks/build.ts +++ b/cli/src/tasks/build.ts @@ -30,8 +30,7 @@ export async function buildCommand( } const buildCommandOptions: BuildCommandOptions = { - scheme: - buildOptions.scheme || config.ios.scheme, + scheme: buildOptions.scheme || config.ios.scheme, keystorepath: buildOptions.keystorepath || config.android.buildOptions.keystorePath, keystorepass: