How to build a one-click deployment in libGDX 1.0 (II)

posted in: Development | 0

My last post about this topic was a bit brief and perhaps some concepts didn’t remain clear. So here you have, this is part II. Hope you enjoy it!

These are the contents:

– A look at the repository where the code of one-click-deployment with libGDX is.
– Learn how to distribute our package anywhere.
– We’ll see an alternative on Unity3D

One-Click-Deployment on libGDX

For the impatient people here is the repository with a sample project.

First we include this duplicate code in the android/build.gradle and ios/build.gradle:

* A versioning software reader:

[java] // Read current version from properties file
ext.versionFile = file(‘version.properties’)

task loadVersion {
    project.version = readVersion()
}

ProjectVersion readVersion() {
    logger.quiet ‘Reading the android version file.’

    if (!versionFile.exists()) {
        throw new GradleException("Required version file does not exit: $versionFile.canonicalPath")
    }

    Properties versionProps = new Properties()

    versionFile.withInputStream { stream ->
        versionProps.load(stream)
    }

    new ProjectVersion(versionProps.major.toInteger(), versionProps.minor.toInteger(), versionProps.patch.toInteger(), versionProps.build.toInteger())
}
[/java]

* Auto-increment version tasks:

[java] // Control version
task incrementMajorVersion(group: ‘versioning’, description: ‘Increments project major version.’) << {
String currentVersion = version.toString()
++version.major
// Reset minor and patch
version.minor = 0
version.patch = 0
String newVersion = version.toString()
logger.info "Incrementing major project version: $currentVersion -> $newVersion"

ant.propertyfile(file: versionFile) {
entry(key: ‘major’, type: ‘int’, operation: ‘+’, value: 1)
entry(key: ‘minor’, type: ‘int’, operation: ‘=’, value: 0)
entry(key: ‘patch’, type: ‘int’, operation: ‘=’, value: 0)
}
}

task incrementMinorVersion(group: ‘versioning’, description: ‘Increments project minor version.’) << {
String currentVersion = version.toString()
++version.minor
// Reset patch
version.patch = 0
String newVersion = version.toString()
logger.info "Incrementing minor project version: $currentVersion -> $newVersion"

ant.propertyfile(file: versionFile) {
entry(key: ‘minor’, type: ‘int’, operation: ‘+’, value: 1)
entry(key: ‘patch’, type: ‘int’, operation: ‘=’, value: 0)
}
}

task incrementPatchVersion(group: ‘versioning’, description: ‘Increments project patch version.’) << {
String currentVersion = version.toString()
++version.patch
String newVersion = version.toString()
logger.info "Incrementing patch project version: $currentVersion -> $newVersion"

ant.propertyfile(file: versionFile) {
entry(key: ‘patch’, type: ‘int’, operation: ‘+’, value: 1)
}
}

task incrementBuildVersion(group: ‘versioning’, description: ‘Increments project build version.’) << {
String currentVersion = version.toString()
++version.build
String newVersion = version.toString()
logger.info "Incrementing build project version: $currentVersion -> $newVersion"

ant.propertyfile(file: versionFile) {
entry(key: ‘build’, type: ‘int’, operation: ‘+’, value: 1)
}
}
[/java]

* At the end a ProjectVersion Class Definition:

[java] // Class definition
class ProjectVersion {

    Integer major

    Integer minor

    Integer patch

    Integer build

    ProjectVersion(Integer major, Integer minor, Integer patch, Integer build) {
        this.major = major
        this.minor = minor
        this.patch = patch
        this.build = build
    }

    String getVersionName() {
        this.major + "." + this.minor + "." + this.patch
    }

    String getVersionCode() {
Integer.parseInt(new Date().format(‘yyyyMMdd’)) + this.build
    }

    @Override
    String toString() {
        this.getVersionName() + "_" + this.getVersionCode()
    }
}
[/java]

Then both in the android and ios we create two properties files called `version.properties` that contains this:

[text] major=0
minor=0
patch=0
build=0
[/text]

And add define global properties at the root of the project in your gradle.properties file:

Testflight configuration

[text] iosBuildPath=ios/build/robovm/
iosAppName=IOSLauncher
testflightURL=http://testflightapp.com/api/builds.json
testflightAT=YOUR_API_TOKEN
testflightTT=YOUR_TEAM_TOKEN
testflightDefaultNote=This build was uploaded via the upload API and Gradle
testflightNotify=True
testflightDL=YOUR_DISTRIBUTE_LIST_NAMES_SEPARATES_BY_COMMAS
[/text]

Testfairy configuration

[text] androidBuildPath=android/build/apk/
testfairyURL=https://app.testfairy.com/api/upload
testfairyAK=YOUR_API_KEY
androidAppName=android-release
testfairyProguardPath=android/proguard-project.txt
testfairyTG=YOUR_GROUP_NAMES_SEPARATES_BY_COMMAS
testfairyMetrics=cpu,memory,network,logcat
testfairyVideo=off
testfairyMD=10m
testfairyVQ=low
testfairyVR=1.0
testfairyIW=off
testfairyComment=This build was uploaded via the upload API and Gradle
[/text]

Next we configure individual tasks for android/build.gradle.

Locate project `android` and add a signing configuration to sign our game with default android keystore values. It should look like this:

[java] android {
   
// …

    signingConfigs {
        release {
            storeFile file( System.getProperty("user.home") + "/.android/debug.keystore")
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }

}
[/java]

Next you insert a dependency before `preReleaseBuild` to auto-increment build version with each build.

[java] task updateAndroidManifestXML(group: ‘versioning’, description: ‘Updates AndroidManifest.xml project file.’) << {

def manifestFile = file("AndroidManifest.xml")

def pattern = java.util.regex.Pattern.compile("versionCode="(\d+)"")
def manifestContent = manifestFile.getText()
def matcher = pattern.matcher(manifestContent)
matcher.find()
manifestContent = matcher.replaceFirst("versionCode="" + version.getVersionCode() + """)

pattern = java.util.regex.Pattern.compile("versionName="(.*)"")
matcher = pattern.matcher(manifestContent)
matcher.find()
manifestContent = matcher.replaceFirst("versionName="" + version.getVersionName() + """)

manifestFile.write(manifestContent)

}

// Autoincrement build versioning
updateAndroidManifestXML.dependsOn incrementBuildVersion

tasks.whenTaskAdded { task ->
if( task.name == ‘preReleaseBuild’ )
task.dependsOn updateAndroidManifestXML
}
[/java]

And finally we include a task to upload and distribute our game using testfairy

[java] task execTestfairyUpload (type:Exec) {

def currentWorkingDir = System.getProperty("user.dir")
commandLine ‘curl’
args testfairyURL,
‘-F’,
‘api_key=’ + testfairyAK,
‘-F’,
‘apk_file=@’ + currentWorkingDir + ‘/’ + androidBuildPath + androidAppName + ‘.apk’,
‘-F’,
‘proguard_file=@’ + currentWorkingDir + ‘/’ + testfairyProguardPath,
‘-F’,
‘testers_groups=’ + testfairyTG,
‘-F’,
‘metrics=’ + testfairyMetrics,
‘-F’,
‘max-duration=’ + testfairyMD,
‘-F’,
‘video=’ + testfairyVideo,
‘-F’,
‘video-quality=’ + testfairyVQ,
‘-F’,
‘video-rate=’ + testfairyVR,
‘-F’,
‘icon-watermark=’ + testfairyIW,
‘-F’,
‘comment=’ + testfairyComment
//store the output instead of printing to the console:
standardOutput = new ByteArrayOutputStream()

//extension method execTestfairyUpload.output() can be used to obtain the output:
ext.output = {
return standardOutput.toString()
}

}

task testfairyUpload(dependsOn: execTestfairyUpload) << {
logger.info "${execTestfairyUpload.output()}"
}
[/java]

Now’s the turn of ios/build.gradle. Open this and modify createIPA task so it depends on an update of robovm.properties.
As we shall see below, `updateRoboVMProperties` task will update `app.version` and `app.build`.
Also `updateRoboVMProperties` task depends on a increment build version task:

[java] – createIPA.dependsOn build

+ // Update info.plist.xml
+ task updateRoboVMProperties << {
+
+ ant.propertyfile(file: robovmFile) {
+ entry(key: ‘app.version’, type: ‘string’, operation: ‘=’, value: version.getVersionName())
+ }
+
+ ant.propertyfile(file: robovmFile) {
+ entry(key: ‘app.build’, type: ‘int’, operation: ‘=’, value: version.build.toString())
+ }
+
+ }

+ // Autoincrement build versioning
+ incrementBuildVersion.dependsOn build
+ updateRoboVMProperties.dependsOn incrementBuildVersion
+ createIPA.dependsOn updateRoboVMProperties
[/java]

And also finally we include a task to zip dSYM, upload and distribute our game using testflight in this occasion

[java] task execTestflightZip (type:Exec) {

def currentWorkingDir = System.getProperty("user.dir")
commandLine ‘/usr/bin/zip’
args ‘-r’,
currentWorkingDir + ‘/’ + iosBuildPath + ‘IOSLauncher.app.dSYM.zip’,
currentWorkingDir + ‘/’ + iosBuildPath + ‘IOSLauncher.app.dSYM’
//store the output instead of printing to the console:
standardOutput = new ByteArrayOutputStream()

//extension method execTestflightDistribute.output() can be used to obtain the output:
ext.output = {
return standardOutput.toString()
}

}

task testflightZip(dependsOn: execTestflightZip) << {
logger.info "${execTestflightZip.output()}"
}

task execTestflightUpload (type:Exec, dependsOn: testflightZip) {

def currentWorkingDir = System.getProperty("user.dir")
commandLine ‘curl’
args testflightURL,
‘-F’,
‘file=@’ + currentWorkingDir + ‘/’ + iosBuildPath + iosAppName + ‘.ipa’,
‘-F’,
‘dsym=@’ + currentWorkingDir + ‘/’ + iosBuildPath + iosAppName + ‘.app.dSYM.zip’,
‘-F’,
‘api_token=’ + testflightAT,
‘-F’,
‘team_token=’ + testflightTT,
‘-F’,
‘notes=’ + testflightDefaultNote,
‘-F’,
‘notify=’ + testflightNotify,
‘-F’,
‘distribution_lists=’ + testflightDL
//store the output instead of printing to the console:
standardOutput = new ByteArrayOutputStream()

//extension method execTestflightDistribute.output() can be used to obtain the output:
ext.output = {
return standardOutput.toString()
}

}

task testflightUpload(dependsOn: execTestflightUpload) << {
logger.info "${execTestflightUpload.output()}"
}
[/java]

Distribute anywhere

Lack of resources is a major problem that faces any indie studio.

Packaging and distributing tasks only need a commandline so we’ll configurate our MAC to access remotely by SSH from any system, including your phone for an emergency.

There are hundreds of tutorials on how to do this. Here’s an example. I’m just giving an example of the potential for our approach.

Make sure you have the Mac environment properly configured according to libGDX WIKI and here a set of examples:

[text] ssh user@server
// First increment minor version, increment build version, create *.ipa and distribute using testflight
server:~ user$ ./gradlew ios:incrementMinorVersion ios:createIPA ios:testflightUpload
// Increment build version, create *.apk and distribute using testfairy
server:~ user$ ./gradlew android:assembleRelease android:testfairyUpload
[/text]

IMPORTANT: Testers with tethered phones can’t work with testflight. An alternative is distribute our *.ipa with an a cloud storage service and they install using iFunbox.

 

One-Click-Deployment on Unity3D

Finally we’ll see a plugin for Unity3D includes all of the above except uploading to testflight and testfairy… but you can reuse this.

The project of plugin is hosted HERE.

We have to create a new set of configurations for android and iOS devices: Assets > Create > UBS Build Collection

bit

You can define pre/post build steps, like auto incrementing the version (also you can do it manually).

When you are prepared to generate a new version press `Run selected builds` button and the system takes over again from now on.

The only manual step involved is hitting the build button by yourself for each platform. This is a restriction of Unity free version. If your are in pro all process will be automatic.

Finally reuse uploading tasks from the first part of this post.

 

Leave a Reply