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
+44
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.
+24
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: ''
@@ -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);
@@ -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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d51cf8acfff8c941bb753e82750b60a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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);
} }
} }
@@ -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;
} }
} }
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+14
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
+12 -3
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;
+50
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);
+11 -1
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 \
+26
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';
+83
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');