Add Android Build Settings

This commit is contained in:
David Finol 2020-07-05 20:41:21 -05:00 committed by Webber Takken
parent 3523c6a934
commit 6ece6447b2
14 changed files with 307 additions and 6 deletions

View File

@ -301,6 +301,50 @@ Configure the android `versionCode`.
When not specified, the version code is generated from the version using the `major * 1000000 + minor * 1000 + patch` scheme; When not specified, the version code is generated from the version using the `major * 1000000 + minor * 1000 + patch` scheme;
#### androidAppBundle
Set this flag to `true` to build '.aab' instead of '.apk'.
_**required:** `false`_
_**default:** `false`_
#### androidKeystoreName
Configure the android `keystoreName`.
_**required:** `false`_
_**default:** ""_
#### androidKeystoreBase64
Configure the base64 contents of the android keystore file.
The contents will be decoded from base64 with `echo $androidKeystoreBase64 | base64 --decode > $androidKeystoreName`;
_**required:** `false`_
_**default:** ""_
#### androidKeystorePass
Configure the android `keystorePass`.
_**required:** `false`_
_**default:** ""_
#### androidKeyaliasName
Configure the android `keyaliasName`.
_**required:** `false`_
_**default:** ""_
#### androidKeyaliasPass
Configure the android `keyaliasPass`.
_**required:** `false`_
_**default:** ""_
#### allowDirtyBuild #### allowDirtyBuild
Allows the branch of the build to be dirty, and still generate the build. Allows the branch of the build to be dirty, and still generate the build.

View File

@ -38,6 +38,30 @@ inputs:
required: false required: false
default: '' default: ''
description: 'The android versionCode' description: 'The android versionCode'
androidAppBundle:
required: false
default: false
description: 'Whether to build .aab instead of .apk'
androidKeystoreName:
required: false
default: ''
description: 'The android keystoreName'
androidKeystoreBase64:
required: false
default: ''
description: 'The base64 contents of the android keystore file'
androidKeystorePass:
required: false
default: ''
description: 'The android keystorePass'
androidKeyaliasName:
required: false
default: ''
description: 'The android keyaliasName'
androidKeyaliasPass:
required: false
default: ''
description: 'The android keyaliasPass'
customParameters: customParameters:
required: false required: false
default: '' default: ''

View File

@ -29,6 +29,10 @@ namespace UnityBuilderAction
// Set version for this build // Set version for this build
VersionApplicator.SetVersion(options["version"]); VersionApplicator.SetVersion(options["version"]);
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]); VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
// Apply Android settings
if (buildOptions.target == BuildTarget.Android)
AndroidSettings.Apply(options);
// Perform build // Perform build
BuildReport buildReport = BuildPipeline.BuildPlayer(buildOptions); BuildReport buildReport = BuildPipeline.BuildPlayer(buildOptions);

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using UnityEditor;
namespace UnityBuilderAction.Input
{
public class AndroidSettings
{
public static void Apply(Dictionary<string, string> options)
{
EditorUserBuildSettings.buildAppBundle = options["customBuildPath"].EndsWith(".aab");
if (options.TryGetValue("androidKeystoreName", out string keystoreName) && !string.IsNullOrEmpty(keystoreName))
PlayerSettings.Android.keystoreName = keystoreName;
if (options.TryGetValue("androidKeystorePass", out string keystorePass) && !string.IsNullOrEmpty(keystorePass))
PlayerSettings.Android.keystorePass = keystorePass;
if (options.TryGetValue("androidKeyaliasName", out string keyaliasName) && !string.IsNullOrEmpty(keyaliasName))
PlayerSettings.Android.keyaliasName = keyaliasName;
if (options.TryGetValue("androidKeyaliasPass", out string keyaliasPass) && !string.IsNullOrEmpty(keyaliasPass))
PlayerSettings.Android.keyaliasPass = keyaliasPass;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d51cf8acfff8c941bb753e82750b60a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using UnityEditor; using UnityEditor;
namespace UnityBuilderAction.Input namespace UnityBuilderAction.Input
@ -7,6 +8,7 @@ namespace UnityBuilderAction.Input
public class ArgumentsParser public class ArgumentsParser
{ {
static string EOL = Environment.NewLine; static string EOL = Environment.NewLine;
static readonly string[] Secrets = { "androidKeystorePass", "androidKeyaliasName", "androidKeyaliasPass" };
public static Dictionary<string, string> GetValidatedOptions() public static Dictionary<string, string> GetValidatedOptions()
{ {
@ -66,9 +68,11 @@ namespace UnityBuilderAction.Input
// Parse optional value // Parse optional value
bool flagHasValue = next < args.Length && !args[next].StartsWith("-"); bool flagHasValue = next < args.Length && !args[next].StartsWith("-");
string value = flagHasValue ? args[next].TrimStart('-') : ""; string value = flagHasValue ? args[next].TrimStart('-') : "";
bool secret = Secrets.Contains(flag);
string displayValue = secret ? "*HIDDEN*" : "\"" + value + "\"";
// Assign // Assign
Console.WriteLine($"Found flag \"{flag}\" with value \"{value}\"."); Console.WriteLine($"Found flag \"{flag}\" with value {displayValue}.");
providedArguments.Add(flag, value); providedArguments.Add(flag, value);
} }
} }

View File

@ -21,6 +21,7 @@ namespace UnityBuilderAction.Versioning
static void Apply(string version) static void Apply(string version)
{ {
PlayerSettings.bundleVersion = version; PlayerSettings.bundleVersion = version;
PlayerSettings.macOS.buildNumber = version;
} }
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -62,6 +62,16 @@ else
# #
fi fi
#
# Create Android keystore, if needed
#
if [[ -z $ANDROID_KEYSTORE_NAME || -z $ANDROID_KEYSTORE_BASE64 ]]; then
echo "Not creating Android keystore."
else
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$ANDROID_KEYSTORE_NAME"
echo "Created Android keystore."
fi
# #
# Display custom parameters # Display custom parameters
# #
@ -111,6 +121,10 @@ xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
-executeMethod "$BUILD_METHOD" \ -executeMethod "$BUILD_METHOD" \
-version "$VERSION" \ -version "$VERSION" \
-androidVersionCode "$ANDROID_VERSION_CODE" \ -androidVersionCode "$ANDROID_VERSION_CODE" \
-androidKeystoreName "$ANDROID_KEYSTORE_NAME" \
-androidKeystorePass "$ANDROID_KEYSTORE_PASS" \
-androidKeyaliasName "$ANDROID_KEYALIAS_NAME" \
-androidKeyaliasPass "$ANDROID_KEYALIAS_PASS" \
$CUSTOM_PARAMETERS $CUSTOM_PARAMETERS
# Catch exit code # Catch exit code

View File

@ -5,7 +5,11 @@ import Versioning from './versioning';
class BuildParameters { class BuildParameters {
static async create() { static async create() {
const buildFile = this.parseBuildFile(Input.buildName, Input.targetPlatform); const buildFile = this.parseBuildFile(
Input.buildName,
Input.targetPlatform,
Input.androidAppBundle,
);
const buildVersion = await Versioning.determineVersion( const buildVersion = await Versioning.determineVersion(
Input.versioningStrategy, Input.versioningStrategy,
Input.specifiedVersion, Input.specifiedVersion,
@ -26,17 +30,22 @@ class BuildParameters {
buildMethod: Input.buildMethod, buildMethod: Input.buildMethod,
buildVersion, buildVersion,
androidVersionCode, androidVersionCode,
androidKeystoreName: Input.androidKeystoreName,
androidKeystoreBase64: Input.androidKeystoreBase64,
androidKeystorePass: Input.androidKeystorePass,
androidKeyaliasName: Input.androidKeyaliasName,
androidKeyaliasPass: Input.androidKeyaliasPass,
customParameters: Input.customParameters, customParameters: Input.customParameters,
}; };
} }
static parseBuildFile(filename, platform) { static parseBuildFile(filename, platform, androidAppBundle) {
if (Platform.isWindows(platform)) { if (Platform.isWindows(platform)) {
return `${filename}.exe`; return `${filename}.exe`;
} }
if (Platform.isAndroid(platform)) { if (Platform.isAndroid(platform)) {
return `${filename}.apk`; return androidAppBundle ? `${filename}.aab` : `${filename}.apk`;
} }
return filename; return filename;

View File

@ -103,11 +103,21 @@ describe('BuildParameters', () => {
test.each([Platform.types.Android])('appends apk for %s', async (targetPlatform) => { test.each([Platform.types.Android])('appends apk for %s', async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false);
await expect(BuildParameters.create()).resolves.toEqual( await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}.apk` }), expect.objectContaining({ buildFile: `${targetPlatform}.apk` }),
); );
}); });
test.each([Platform.types.Android])('appends aab for %s', async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(true);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ buildFile: `${targetPlatform}.aab` }),
);
});
it('returns the build method', async () => { it('returns the build method', async () => {
const mockValue = 'Namespace.ClassName.BuildMethod'; const mockValue = 'Namespace.ClassName.BuildMethod';
jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue); jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
@ -116,6 +126,46 @@ describe('BuildParameters', () => {
); );
}); });
it('returns the android keystore name', async () => {
const mockValue = 'keystore.keystore';
jest.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ androidKeystoreName: mockValue }),
);
});
it('returns the android keystore base64-encoded content', async () => {
const mockValue = 'secret';
jest.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ androidKeystoreBase64: mockValue }),
);
});
it('returns the android keystore pass', async () => {
const mockValue = 'secret';
jest.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ androidKeystorePass: mockValue }),
);
});
it('returns the android keyalias name', async () => {
const mockValue = 'secret';
jest.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ androidKeyaliasName: mockValue }),
);
});
it('returns the android keyalias pass', async () => {
const mockValue = 'secret';
jest.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(
expect.objectContaining({ androidKeyaliasPass: mockValue }),
);
});
it('returns the custom parameters', async () => { it('returns the custom parameters', async () => {
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue'; const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue); jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);

View File

@ -28,8 +28,13 @@ class Docker {
buildFile, buildFile,
buildMethod, buildMethod,
buildVersion, buildVersion,
customParameters,
androidVersionCode, androidVersionCode,
androidKeystoreName,
androidKeystoreBase64,
androidKeystorePass,
androidKeyaliasName,
androidKeyaliasPass,
customParameters,
} = parameters; } = parameters;
const command = `docker run \ const command = `docker run \
@ -49,6 +54,11 @@ class Docker {
--env BUILD_METHOD="${buildMethod}" \ --env BUILD_METHOD="${buildMethod}" \
--env VERSION="${buildVersion}" \ --env VERSION="${buildVersion}" \
--env ANDROID_VERSION_CODE="${androidVersionCode}" \ --env ANDROID_VERSION_CODE="${androidVersionCode}" \
--env ANDROID_KEYSTORE_NAME="${androidKeystoreName}" \
--env ANDROID_KEYSTORE_BASE64="${androidKeystoreBase64}" \
--env ANDROID_KEYSTORE_PASS="${androidKeystorePass}" \
--env ANDROID_KEYALIAS_NAME="${androidKeyaliasName}" \
--env ANDROID_KEYALIAS_PASS="${androidKeyaliasPass}" \
--env CUSTOM_PARAMETERS="${customParameters}" \ --env CUSTOM_PARAMETERS="${customParameters}" \
--env HOME=/github/home \ --env HOME=/github/home \
--env GITHUB_REF \ --env GITHUB_REF \

View File

@ -45,6 +45,32 @@ class Input {
return core.getInput('androidVersionCode'); return core.getInput('androidVersionCode');
} }
static get androidAppBundle() {
const input = core.getInput('androidAppBundle') || 'false';
return input === 'true' ? 'true' : 'false';
}
static get androidKeystoreName() {
return core.getInput('androidKeystoreName') || '';
}
static get androidKeystoreBase64() {
return core.getInput('androidKeystoreBase64') || '';
}
static get androidKeystorePass() {
return core.getInput('androidKeystorePass') || '';
}
static get androidKeyaliasName() {
return core.getInput('androidKeyaliasName') || '';
}
static get androidKeyaliasPass() {
return core.getInput('androidKeyaliasPass') || '';
}
static get allowDirtyBuild() { static get allowDirtyBuild() {
const input = core.getInput('allowDirtyBuild') || 'false'; const input = core.getInput('allowDirtyBuild') || 'false';

View File

@ -131,6 +131,89 @@ describe('Input', () => {
}); });
}); });
describe('androidAppBundle', () => {
it('returns the default value', () => {
expect(Input.androidAppBundle).toStrictEqual('false');
});
it('returns true when string true is passed', () => {
const spy = jest.spyOn(core, 'getInput').mockReturnValue('true');
expect(Input.androidAppBundle).toStrictEqual('true');
expect(spy).toHaveBeenCalledTimes(1);
});
it('returns false when string false is passed', () => {
const spy = jest.spyOn(core, 'getInput').mockReturnValue('false');
expect(Input.androidAppBundle).toStrictEqual('false');
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('androidKeystoreName', () => {
it('returns the default value', () => {
expect(Input.androidKeystoreName).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'keystore.keystore';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.androidKeystoreName).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('androidKeystoreBase64', () => {
it('returns the default value', () => {
expect(Input.androidKeystoreBase64).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'secret';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.androidKeystoreBase64).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('androidKeystorePass', () => {
it('returns the default value', () => {
expect(Input.androidKeystorePass).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'secret';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.androidKeystorePass).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('androidKeyaliasName', () => {
it('returns the default value', () => {
expect(Input.androidKeyaliasName).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'secret';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.androidKeyaliasName).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('androidKeyaliasPass', () => {
it('returns the default value', () => {
expect(Input.androidKeyaliasPass).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'secret';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.androidKeyaliasPass).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('allowDirtyBuild', () => { describe('allowDirtyBuild', () => {
it('returns the default value', () => { it('returns the default value', () => {
expect(Input.allowDirtyBuild).toStrictEqual('false'); expect(Input.allowDirtyBuild).toStrictEqual('false');