/** * Build script for android app. * * Besides options below this script may require signing parameters passed as environment variables: * 'APK_SIGN_ALIAS' * 'APK_SIGN_STORE_PASS' * 'APK_SIGN_KEY_PASS' * 'APK_SIGN_STORE' * 'ANDROID_HOME' */ import { Argument, Option, program } from "commander" import { runDevBuild } from "./buildSrc/DevBuild.js" import { prepareMobileBuild } from "./buildSrc/prepareMobileBuild.js" import { buildWebapp } from "./buildSrc/buildWebapp.js" import { getTutanotaAppVersion, measure } from "./buildSrc/buildUtils.js" import path from "node:path" import { $, cd } from "zx" const log = (...messages) => console.log(chalk.green("\nBUILD:"), ...messages, "\n") await program .usage("[options] [test|prod|local|host ] ") .addArgument( new Argument("stage", "the server to connect to. test/local/prod are shorthands for using host of the corresponding staging level server") .choices(["test", "prod", "local", "host"]) .default("prod") .argOptional(), ) .addArgument(new Argument("host").argOptional()) .addOption(new Option("-a, --app ", "app to build").choices(["mail", "calendar"]).default("mail")) .addOption( new Option( "-b, --buildtype ", "gradle build type. use debug if you need to debug the app with android studio. release and releaseTest build the same app with different appIds for side-by-side installation", ) .choices(["debug", "release", "releaseTest"]) .default("release"), ) .addOption(new Option("-i, --install", "call adb install after build to deploy the app to a device/emulator")) .addOption( new Option( "-w --webclient ", "choose web client build. make is faster and easier to debug, but dist is what would be running in production. There's usually no reason to use dist during development.", ) .choices(["make", "dist"]) .default("dist"), ) .option("-e, --existing", "Use existing prebuilt web client files to skip the lengthy web client build. Use if you're developing the Kotlin code.") .action(async (stage, host, { webclient, buildtype, install, existing, app }) => { if ((stage === "host" && host == null) || (stage !== "host" && host != null)) { program.outputHelp() process.exit(1) } const apk = await buildAndroid({ stage: stage ?? "prod", host: host, webClient: webclient, existing, buildType: buildtype, app, }) if (install) { await $`adb install ${apk}` // would be cool to auto-start the app, but needs to figure out the correct app to start: // await $`adb shell am start -n de.tutao.tutanota/de.tutao.tutanota.MainActivity` } }) .parseAsync(process.argv) async function buildCalendarBundle({ buildType }) { const { version } = JSON.parse(await $`cat package.json`.quiet()) const bundleName = `calendar-tutao-${buildType}-${version}.aab` const bundlePath = `app-android/calendar/build/outputs/bundle/tutao${buildType.charAt(0).toUpperCase() + buildType.slice(1)}/${bundleName}` const outPath = `./build-calendar-app/app-android/${bundleName}` cd("./app-android") await $`./gradlew :calendar:bundleTutao${buildType}` cd("..") await $`mkdir -p build-calendar-app/app-android` await $`mv ${bundlePath} ${outPath}` log(`Build complete. The AAB is located at: ${outPath}`) return outPath } async function buildCalendarApk({ buildType }) { const { version } = JSON.parse(await $`cat package.json`.quiet()) const bundleName = `calendar-tutao-${buildType}-${version}` const bundlePath = `app-android/calendar/build/outputs/apk/tutao/${buildType}/${bundleName}` const outPath = `./build-calendar-app/app-android/${bundleName}` cd("./app-android") await $`if [ -f .${outPath}.aab ]; then mkdir ../temp; mv .${outPath}.aab ../temp/${bundleName}.aab; fi` await $`./gradlew :calendar:assembleTutao${buildType}` cd("..") await $`mkdir -p build-calendar-app/app-android` await $`mv ${bundlePath}.apk ${outPath}.apk` await $`if [ -f ./temp/${bundleName}.aab ]; then mv ./temp/${bundleName}.aab ${outPath}.aab; rm -d ./temp; fi` log(`Build complete. The APK is located at: ${outPath}`) return outPath } async function buildMailApk({ buildType }) { const { version } = JSON.parse(await $`cat package.json`.quiet()) const apkName = `tutanota-app-tutao-${buildType}-${version}.apk` const apkPath = `app-android/app/build/outputs/apk/tutao/${buildType}/${apkName}` const outPath = `./build/app-android/${apkName}` cd("./app-android") await $`./gradlew :app:assembleTutao${buildType}` cd("..") await $`mkdir -p build/app-android` await $`mv ${apkPath} ${outPath}` log(`Build complete. The APK is located at: ${outPath}`) return outPath } async function buildAndroid({ stage, host, buildType, existing, webClient, app }) { log(`Starting ${stage} build with build type: ${buildType}, webclient: ${webClient}, host: ${host}`) if (!existing) { if (webClient === "make") { await runDevBuild({ stage, host, desktop: false, clean: false, watch: false, serve: false, networkDebugging: false, app, }) } else { const version = await getTutanotaAppVersion() await buildWebapp({ version, stage, host, minify: true, projectDir: path.resolve("."), measure, app, mobileBuild: true, }) } } else { console.log("skipped webapp build") } await prepareMobileBuild({ app }) const buildDir = app === "mail" ? "build" : "build-calendar-app" try { await $`rm -r ${buildDir}/app-android` } catch (e) { // Ignoring the error if the folder is not there } if (app === "mail") { return await buildMailApk({ buildType }) } else { await buildCalendarBundle({ buildType }) return await buildCalendarApk({ buildType }) } }