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;
#### 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
Allows the branch of the build to be dirty, and still generate the build.

View File

@ -38,6 +38,30 @@ inputs:
required: false
default: ''
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:
required: false
default: ''

View File

@ -29,6 +29,10 @@ namespace UnityBuilderAction
// Set version for this build
VersionApplicator.SetVersion(options["version"]);
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
// Apply Android settings
if (buildOptions.target == BuildTarget.Android)
AndroidSettings.Apply(options);
// Perform build
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.Collections.Generic;
using System.Linq;
using UnityEditor;
namespace UnityBuilderAction.Input
@ -7,6 +8,7 @@ namespace UnityBuilderAction.Input
public class ArgumentsParser
{
static string EOL = Environment.NewLine;
static readonly string[] Secrets = { "androidKeystorePass", "androidKeyaliasName", "androidKeyaliasPass" };
public static Dictionary<string, string> GetValidatedOptions()
{
@ -66,9 +68,11 @@ namespace UnityBuilderAction.Input
// Parse optional value
bool flagHasValue = next < args.Length && !args[next].StartsWith("-");
string value = flagHasValue ? args[next].TrimStart('-') : "";
bool secret = Secrets.Contains(flag);
string displayValue = secret ? "*HIDDEN*" : "\"" + value + "\"";
// Assign
Console.WriteLine($"Found flag \"{flag}\" with value \"{value}\".");
Console.WriteLine($"Found flag \"{flag}\" with value {displayValue}.");
providedArguments.Add(flag, value);
}
}

View File

@ -21,6 +21,7 @@ namespace UnityBuilderAction.Versioning
static void Apply(string 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
#
# 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
#
@ -111,6 +121,10 @@ xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
-executeMethod "$BUILD_METHOD" \
-version "$VERSION" \
-androidVersionCode "$ANDROID_VERSION_CODE" \
-androidKeystoreName "$ANDROID_KEYSTORE_NAME" \
-androidKeystorePass "$ANDROID_KEYSTORE_PASS" \
-androidKeyaliasName "$ANDROID_KEYALIAS_NAME" \
-androidKeyaliasPass "$ANDROID_KEYALIAS_PASS" \
$CUSTOM_PARAMETERS
# Catch exit code

View File

@ -5,7 +5,11 @@ import Versioning from './versioning';
class BuildParameters {
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(
Input.versioningStrategy,
Input.specifiedVersion,
@ -26,17 +30,22 @@ class BuildParameters {
buildMethod: Input.buildMethod,
buildVersion,
androidVersionCode,
androidKeystoreName: Input.androidKeystoreName,
androidKeystoreBase64: Input.androidKeystoreBase64,
androidKeystorePass: Input.androidKeystorePass,
androidKeyaliasName: Input.androidKeyaliasName,
androidKeyaliasPass: Input.androidKeyaliasPass,
customParameters: Input.customParameters,
};
}
static parseBuildFile(filename, platform) {
static parseBuildFile(filename, platform, androidAppBundle) {
if (Platform.isWindows(platform)) {
return `${filename}.exe`;
}
if (Platform.isAndroid(platform)) {
return `${filename}.apk`;
return androidAppBundle ? `${filename}.aab` : `${filename}.apk`;
}
return filename;

View File

@ -103,11 +103,21 @@ describe('BuildParameters', () => {
test.each([Platform.types.Android])('appends apk for %s', async (targetPlatform) => {
jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false);
await expect(BuildParameters.create()).resolves.toEqual(
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 () => {
const mockValue = 'Namespace.ClassName.BuildMethod';
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 () => {
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);

View File

@ -28,8 +28,13 @@ class Docker {
buildFile,
buildMethod,
buildVersion,
customParameters,
androidVersionCode,
androidKeystoreName,
androidKeystoreBase64,
androidKeystorePass,
androidKeyaliasName,
androidKeyaliasPass,
customParameters,
} = parameters;
const command = `docker run \
@ -49,6 +54,11 @@ class Docker {
--env BUILD_METHOD="${buildMethod}" \
--env VERSION="${buildVersion}" \
--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 HOME=/github/home \
--env GITHUB_REF \

View File

@ -45,6 +45,32 @@ class Input {
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() {
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', () => {
it('returns the default value', () => {
expect(Input.allowDirtyBuild).toStrictEqual('false');